From d8371a7b4171f2f7709513850d4d32aab7a7aa81 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: Sat, 25 Jul 2026 00:59:01 +0800 Subject: [PATCH 01/24] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E5=86=B7=E5=90=AF=E5=8A=A8=E5=B0=B1=E7=BB=AA=E7=8A=B6?= =?UTF-8?q?=E6=80=81=20/=20fix=20camera=20cold-start=20readiness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一原始帧探测与 Core 启动返回的真实就绪状态,并覆盖空闲预热与重新探测。 / Align raw-frame probing with Core readiness responses and cover idle warm-up plus re-probing. --- ogscope/core/application/core_service.py | 14 ++++++-- ogscope/web/camera_shared.py | 15 +++++++-- tests/unit/test_camera_manager_health.py | 42 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/ogscope/core/application/core_service.py b/ogscope/core/application/core_service.py index bd3db01..f4c97f8 100644 --- a/ogscope/core/application/core_service.py +++ b/ogscope/core/application/core_service.py @@ -329,13 +329,21 @@ async def start_camera(self) -> dict[str, Any]: hardware_client = get_hardware_plane_client() hp_result = await hardware_client.device_command("camera", "start") result = await camera_domain_service.start() + status = await self.get_camera_status() + ready = bool(status.get("connected")) and bool(status.get("streaming")) + message = str(result.get("message", "")) + if not ready: + message = str(status.get("error") or message or "camera did not become ready") return { - "success": bool(result.get("success", True)), - "message": result.get("message", ""), - "info": {}, + "success": bool(result.get("success", True)) and ready, + "message": message, + "info": status.get("info", {}), "applied": { "action": "start", "hardware_plane_ok": bool(hp_result.get("success", False)), + "ready": ready, + "connected": bool(status.get("connected")), + "streaming": bool(status.get("streaming")), }, } diff --git a/ogscope/web/camera_shared.py b/ogscope/web/camera_shared.py index 9b94944..254d000 100644 --- a/ogscope/web/camera_shared.py +++ b/ogscope/web/camera_shared.py @@ -71,6 +71,7 @@ def __init__(self) -> None: self._consecutive_grab_failures = 0 self._stream_started_at = 0.0 self._last_capture_success_mono = 0.0 + self._has_successful_capture = False self._preview_consumers = 0 self._analysis_consumers = 0 self._recording_consumers = 0 @@ -187,6 +188,7 @@ def _read_frame_sync(self): now = time.monotonic() self._capture_sequence += 1 self._last_capture_success_mono = now + self._has_successful_capture = True self._capture_timestamps.append(now) return frame @@ -228,6 +230,7 @@ async def ensure_started(self, *, start_grabber: bool = False) -> None: return if self._camera is None: self._health_error = None + self._has_successful_capture = False self._camera = await asyncio.to_thread(self._create_camera_sync) if self._camera is None: self._health_error = "相机初始化失败 / Camera init failed" @@ -343,6 +346,7 @@ async def stop(self) -> None: ) self._camera = None self._last_capture_success_mono = 0.0 + self._has_successful_capture = False with self._frame_lock: self._latest_raw = None self._latest_jpeg = None @@ -446,6 +450,8 @@ async def _invalidate_camera_locked(self, reason: str) -> None: self._camera = None self._consecutive_grab_failures = 0 self._stream_started_at = 0.0 + self._last_capture_success_mono = 0.0 + self._has_successful_capture = False with self._frame_lock: self._latest_raw = None self._latest_jpeg = None @@ -556,7 +562,10 @@ async def status(self) -> dict[str, Any]: } initialized = bool(getattr(cam, "is_initialized", False)) capturing = bool(getattr(cam, "is_capturing", False)) - has_frames = self._frame_id > 0 + # 空闲相机不会持续抓帧;一次成功探测即可证明流已建立,实际使用时 ensure_started 会再次探测。 + # Idle cameras do not grab continuously; one successful probe proves stream setup, + # while ensure_started re-probes before the next real consumer uses it. + has_successful_capture = self._has_successful_capture within_grace = ( self._stream_started_at > 0 and (time.time() - self._stream_started_at) <= self._probe_timeout_sec @@ -564,7 +573,7 @@ async def status(self) -> dict[str, Any]: offline = ( bool(self._health_error) or self._consecutive_grab_failures >= self._max_grab_failures - or (capturing and not has_frames and not within_grace) + or (capturing and not has_successful_capture and not within_grace) ) if offline: reason = self._health_error @@ -582,7 +591,7 @@ async def status(self) -> dict[str, Any]: info = await asyncio.to_thread(cam.get_camera_info) return { "connected": initialized and capturing, - "streaming": capturing and (has_frames or within_grace), + "streaming": capturing and (has_successful_capture or within_grace), "info": info, "runtime_overrides": self._runtime_overrides, } diff --git a/tests/unit/test_camera_manager_health.py b/tests/unit/test_camera_manager_health.py index 738c830..f33d688 100644 --- a/tests/unit/test_camera_manager_health.py +++ b/tests/unit/test_camera_manager_health.py @@ -67,6 +67,48 @@ async def test_ensure_started_succeeds_when_frames_available() -> None: await manager.stop() +@pytest.mark.asyncio +async def test_status_uses_successful_raw_probe_without_jpeg_grabber() -> None: + """冷启动探测帧应直接建立流健康状态 / Cold-start raw probe must establish stream health.""" + manager = CameraManager() + manager._probe_timeout_sec = 0.5 + camera = _FrameCamera() + manager.attach_camera_instance(camera) + + await manager.ensure_started() + assert manager._frame_id == 0 + + status = await manager.status() + assert status["connected"] is True + assert status["streaming"] is True + await manager.stop() + + +@pytest.mark.asyncio +async def test_stale_camera_reprobe_restores_status_without_jpeg_grabber() -> None: + """过期采集重新探测后应一次恢复 / A stale capture should recover after one re-probe.""" + manager = CameraManager() + manager._probe_timeout_sec = 0.5 + manager._stale_timeout_sec = 0.5 + camera = _FrameCamera() + manager.attach_camera_instance(camera) + + await manager.ensure_started() + manager._last_capture_success_mono -= 2.0 + + idle_status = await manager.status() + assert idle_status["connected"] is True + assert idle_status["streaming"] is True + + await manager.ensure_started() + + status = await manager.status() + assert camera.read_count == 2 + assert status["connected"] is True + assert status["streaming"] is True + await manager.stop() + + @pytest.mark.asyncio async def test_ensure_started_fast_path_does_not_probe_again() -> None: """新鲜相机重复ensure不应额外抓帧 / Fresh repeated ensure must not grab another frame.""" From 09ff17ceaf1e0651b085d2b49ef0dc27b202c681 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: Wed, 29 Jul 2026 12:52:11 +0800 Subject: [PATCH 02/24] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E5=A5=91=E7=BA=A6=E4=B8=8E=E9=A1=B9=E7=9B=AE=E6=8C=87?= =?UTF-8?q?=E5=8D=97=20/=20sync=20camera=20contract=20and=20project=20guid?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 2 +- CONTRIBUTING_EN.md | 2 +- README.md | 7 +- README_EN.md | 8 +- docs/QUICK_START.md | 292 ++++++----------------- docs/QUICK_START_EN.md | 213 ++++++++--------- docs/README.md | 1 + docs/README_EN.md | 1 + docs/contracts/core-rest-v1.md | 2 + docs/contracts/core-rest-v1_EN.md | 2 + docs/development/README.md | 2 +- docs/development/README_EN.md | 2 +- docs/development/SCRIPT_STANDARDS.md | 3 +- docs/development/SCRIPT_STANDARDS_EN.md | 93 ++++++++ ogscope/core/application/core_service.py | 4 +- ogscope/web/api/models/schemas.py | 2 +- tests/unit/test_core_contract_api.py | 42 ++++ 17 files changed, 332 insertions(+), 346 deletions(-) create mode 100644 docs/development/SCRIPT_STANDARDS_EN.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e37ef4..b8dc389 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,7 +132,7 @@ 流程:`feature/*` / `fix/*` → PR → `staging` → 测试通过后 PR → `main`。 -**禁止直接向 `main` 或 `staging` 提交或推送**(本地 pre-commit 会拦截;remote 需配置 GitHub Rulesets 或 Branch protection,见 [.github/scripts/apply-branch-protection.sh](../.github/scripts/apply-branch-protection.sh))。 +**禁止直接向 `main` 或 `staging` 提交或推送**(本地 pre-commit 会拦截;remote 需配置 GitHub Rulesets 或 Branch protection,见 [.github/scripts/apply-branch-protection.sh](.github/scripts/apply-branch-protection.sh))。 **已废弃**:不再使用 `develop`、`dev`、`dev-latest` 集成分支。 diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md index 08f1090..f795e34 100644 --- a/CONTRIBUTING_EN.md +++ b/CONTRIBUTING_EN.md @@ -87,7 +87,7 @@ Thank you for your interest in OGScope. We welcome contributions of all kinds. Flow: `feature/*` / `fix/*` → PR → `staging` → validated PR → `main`. -**Do not commit or push directly to `main` or `staging`.** Local pre-commit blocks this; configure GitHub Rulesets or branch protection via [.github/scripts/apply-branch-protection.sh](../.github/scripts/apply-branch-protection.sh). +**Do not commit or push directly to `main` or `staging`.** Local pre-commit blocks this; configure GitHub Rulesets or branch protection via [.github/scripts/apply-branch-protection.sh](.github/scripts/apply-branch-protection.sh). **Deprecated**: `develop`, `dev`, and `dev-latest` integration branches. diff --git a/README.md b/README.md index ae8dda0..5c05844 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ ### Phase 2 - 完整功能 - ⏳ SPI 屏幕显示 -- ⏳ 自动板块求解 +- ✅ Tetra3 自动板块求解 +- ✅ 实时解算与调试控制台 - ⏳ 移动 App 控制 - ⏳ 校准数据管理 @@ -77,7 +78,7 @@ python -m ogscope.main ### Web 界面访问 -启动后访问: [http://raspberrypi.local:8000](http://raspberrypi.local:8000) 或 http://:8000 +启动后访问:[http://raspberrypi.local:8000](http://raspberrypi.local:8000) 或 `http://<设备IP>:8000` ## 文档 @@ -141,4 +142,4 @@ OGScope/ ## 贡献 -欢迎提交 Issue 和 Pull Request!详见 [贡献指南](CONTRIBUTING.md) \ No newline at end of file +欢迎提交 Issue 和 Pull Request!详见 [贡献指南](CONTRIBUTING.md) diff --git a/README_EN.md b/README_EN.md index b56dd94..5fc4b8a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -22,7 +22,8 @@ English | [中文](README.md) ### Phase 2 - Complete Features - ⏳ SPI screen display -- ⏳ Automatic plate solving +- ✅ Tetra3 automatic plate solving +- ✅ Realtime solve and debug console - ⏳ Mobile app control - ⏳ Calibration data management @@ -43,13 +44,13 @@ English | [中文](README.md) - **Processor**: Raspberry Pi Zero 2W (ARM Cortex-A53) - **Camera**: IMX327 sensor (1920x1080) - **Display**: 2.4" SPI LCD (240x320) -- **Software**: Python 3.9 + FastAPI +- **Software**: Python 3.10+ + FastAPI ## Quick Start ### Requirements -- Python 3.9+ +- Python 3.10+ - Poetry 1.2+ - Raspberry Pi Zero 2W (Raspberry Pi OS) @@ -139,4 +140,3 @@ See [LICENSE](LICENSE) file for details. ## Contributing Issues and Pull Requests are welcome! See [Contributing Guide](CONTRIBUTING.md) for details. - diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md index f943b0c..be455d3 100644 --- a/docs/QUICK_START.md +++ b/docs/QUICK_START.md @@ -1,293 +1,147 @@ # OGScope 快速开始指南 -本指南将帮助你快速搭建 OGScope 开发环境。 - 中文 | [English](QUICK_START_EN.md) -## 🎯 目标 - -- ✅ 在 Raspberry Pi Zero 2W 上运行 OGScope -- ✅ 配置 PyCharm Professional 远程开发 -- ✅ 通过 Web 界面访问系统 +本指南用于在 Raspberry Pi Zero 2W 上完成首次安装和基础验证。完整安装参数、WiFi、低内存优化与日常升级见[开发指南](development/README.md)。 -## 📋 准备工作 +## 1. 准备 -### 硬件要求 +### 硬件 - Raspberry Pi Zero 2W -- IMX327 相机模块 -- MicroSD 卡 (32GB+) -- 5V 3A 电源适配器 -- (可选)2.4寸 SPI LCD 屏幕 +- IMX327 MIPI 相机 +- 32 GB 或更大的 MicroSD 卡 +- 5 V / 2 A 或更高规格的稳定电源 -### 软件要求 +### 软件 -#### 开发机 (Mac) -- macOS -- PyCharm Professional 2021.1.3 或更高版本 +- Raspberry Pi OS Lite 64-bit(Debian/apt 系) +- Python 3.10+ - Git +- 首次安装时可访问 apt、PyPI 和 GitHub -#### Raspberry Pi -- Raspberry Pi OS(官方镜像) -- Python 3.9+ -- 网络连接(WiFi 或有线) - -## 🚀 第一步:Raspberry Pi 系统配置 +建议在 Raspberry Pi Imager 中预先设置主机名、SSH、公钥或初始用户。不要依赖历史默认用户名或默认密码。 -### 1.1 烧录系统 +## 2. 连接开发板 ```bash -# 1. 下载 Raspberry Pi Imager 工具 -# 访问: https://www.raspberrypi.org/downloads/ - -# 2. 使用 Raspberry Pi Imager 烧录 Raspberry Pi OS Lite -# 或使用 dd 命令烧录到 SD 卡 -# macOS/Linux: -sudo dd if=2024-01-15-raspios-bookworm-armhf-lite.img of=/dev/diskX bs=4m status=progress +ssh @raspberrypi.local +# mDNS 不可用时改用路由器分配的 IP +ssh @ ``` -### 1.2 首次启动 +推荐配置 SSH 公钥: ```bash -# 1. 插入 SD 卡并启动 Raspberry Pi -# 2. 默认用户名: pi -# 3. 默认密码: raspberry - -# 首次登录后,建议修改密码 -passwd +ssh-copy-id @ +ssh -o BatchMode=yes @ true ``` -### 1.3 配置网络 +## 3. 克隆并安装 ```bash -# 方法 1: WiFi 连接 -sudo nmcli dev wifi connect "WiFi名称" password "密码" - -# 方法 2: 配置静态 IP(可选) -sudo nano /etc/network/interfaces - -# 查看 IP 地址 -ip addr show wlan0 # WiFi -ip addr show eth0 # 有线 +git clone https://github.com/OG-star-tech/OGScope.git +cd OGScope +chmod +x scripts/bootstrap.sh +./scripts/bootstrap.sh ``` -### 1.4 SSH 访问 +`bootstrap.sh` 会把运行代码部署到默认目录 `/opt/ogscope`,然后调用安装流程创建项目虚拟环境、补齐系统依赖并注册 `ogscope.service`。 + +国内网络可显式选择镜像: ```bash -# 在 Mac 上测试 SSH 连接 -ssh pi@raspberrypi.local -# 或使用 IP 地址 -ssh pi@192.168.1.xxx +export OGSCOPE_MIRROR=cn +./scripts/bootstrap.sh ``` -## 🔧 第二步:安装 OGScope - -### 2.1 自动安装(推荐) +只需要最小运行环境时: ```bash -# 在 Raspberry Pi 上执行 -cd ~ -git clone https://github.com/your-username/OGScope.git -cd OGScope -chmod +x scripts/install.sh -./scripts/install.sh +OGSCOPE_BOOTSTRAP_MODE=min ./scripts/bootstrap.sh ``` -安装脚本会自动: -- 安装系统依赖 -- 安装 Poetry -- 创建 Python 虚拟环境 -- 安装项目依赖 -- 配置 systemd 服务 - -### 2.2 手动安装 - -如果自动安装失败,可以手动执行: +如果代码已经位于目标运行目录,也可以直接执行: ```bash -# 1. 更新系统 -sudo apt update && sudo apt upgrade -y - -# 2. 安装系统依赖 -sudo apt install -y python3.9 python3-pip python3-venv git \ - build-essential libopencv-dev libjpeg-dev libpng-dev \ - libspidev-dev v4l-utils - -# 3. 安装 Poetry -curl -sSL https://install.python-poetry.org | python3 - -echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc -source ~/.bashrc - -# 4. 克隆项目(如果还没有) -cd ~ -git clone https://github.com/your-username/OGScope.git -cd OGScope - -# 5. 安装 Python 依赖 -poetry install - -# 6. 运行测试 -poetry run python -m ogscope.main +./scripts/install.sh ``` -## 💻 第三步:配置 PyCharm 远程开发 - -### 3.1 配置 SSH 免密登录 - -在 **Mac** 上执行: +## 4. 验证 ```bash -# 生成 SSH 密钥(如果还没有) -ssh-keygen -t ed25519 -C "ogscope-dev" - -# 复制公钥到 Raspberry Pi -ssh-copy-id orangepi@orangepi.local - -# 配置 SSH config -cat >> ~/.ssh/config << 'EOF' -Host orangepi - HostName orangepi.local - User orangepi - Port 22 - ForwardAgent yes - ServerAliveInterval 60 -EOF - -# 测试连接 -ssh orangepi +sudo systemctl status ogscope --no-pager +curl -fsS http://127.0.0.1:8000/health +curl -fsS http://127.0.0.1:8000/api/core/v1/system/status ``` -### 3.2 PyCharm 配置 - -建议以 [开发指南](./development/README.md) 中的“远程开发(手动部署 + systemd)”流程为准。 - -**快速版本**: +浏览器访问: -1. **添加远程解释器** - - `File` → `Settings` → `Project` → `Python Interpreter` - - 点击 ⚙️ → `Add...` → `SSH Interpreter` - - Host: `orangepi.local`, User: `orangepi` - - Interpreter: `/home/orangepi/.local/bin/poetry` +- 首页:`http://:8000/` +- 调试控制台:`http://:8000/debug` +- API 文档:`http://:8000/docs` -2. **配置自动部署** - - `Tools` → `Deployment` → `Configuration` - - 添加 SFTP 服务器 - - 设置路径映射 - -3. **启用自动上传** - - `Tools` → `Deployment` → `Automatic Upload` ✅ - -## 🌐 第四步:访问 Web 界面 - -### 4.1 启动服务 +相机检查: ```bash -# 方法 1: 手动启动(开发模式) -cd ~/OGScope -poetry run python -m ogscope.main - -# 方法 2: 使用 systemd(生产模式) -sudo systemctl start ogscope -sudo systemctl status ogscope +rpicam-hello --list-cameras 2>/dev/null || libcamera-hello --list-cameras +curl -fsS http://127.0.0.1:8000/api/core/v1/camera/status ``` -### 4.2 访问界面 +`POST /api/core/v1/camera/start` 只有在相机确认 `connected=true` 且 `streaming=true` 时才返回成功。 -在浏览器中打开: -``` -http://orangepi.local:8000 -# 或使用 IP 地址 -http://192.168.1.xxx:8000 -``` +## 5. 日常更新 -### 4.3 API 文档 - -FastAPI 自动生成的文档: -``` -http://orangepi.local:8000/docs # Swagger UI -http://orangepi.local:8000/redoc # ReDoc +```bash +cd /opt/ogscope +./scripts/board-update.sh ``` -## 🔍 验证安装 - -### 检查清单 - -- [ ] Raspberry Pi 可以正常启动 -- [ ] SSH 可以连接 -- [ ] Poetry 已安装 -- [ ] OGScope 依赖已安装 -- [ ] Web 服务可以启动 -- [ ] 浏览器可以访问界面 -- [ ] PyCharm 可以远程连接 - -### 运行测试 +需要开发依赖时: ```bash -# 在 Raspberry Pi 上 -cd ~/OGScope -poetry run pytest tests/unit/ +OGSCOPE_INSTALL_DEV=1 ./scripts/board-update.sh ``` -## 🐛 故障排除 - -### 问题 1: 找不到 Raspberry Pi +## 6. 本地开发 ```bash -# 方法 1: 使用 IP 地址 -ip addr show wlan0 +poetry install +poetry run pytest -q -# 方法 2: 路由器管理界面查看 -# 方法 3: 使用 nmap 扫描 -nmap -sn 192.168.1.0/24 +cd web/spa +npm ci +npm run build ``` -### 问题 2: Poetry 安装失败 +涉及相机、GPIO、I2C 或 systemd 的修改必须在真实开发板上复验。开发板同步流程见[开发指南](development/README.md)和 `scripts/sync_dev_board.sh`。 -```bash -# 使用 pip 安装(备选) -pip3 install poetry -``` +## 7. 常见问题 -### 问题 3: Web 服务无法启动 +### 服务启动失败 ```bash -# 查看日志 -journalctl -u ogscope -f - -# 检查端口占用 -sudo netstat -tunlp | grep 8000 - -# 手动启动查看错误 -cd ~/OGScope -poetry run python -m ogscope.main +sudo journalctl -u ogscope -b --no-pager -n 200 +sudo systemctl cat ogscope ``` -### 问题 4: PyCharm 无法连接 +### 相机不可用 ```bash -# 检查 SSH 服务 -sudo systemctl status ssh - -# 检查防火墙 -sudo ufw status - -# 测试 SSH 连接 -ssh -v orangepi@orangepi.local +rpicam-hello --list-cameras 2>/dev/null || libcamera-hello --list-cameras +sudo journalctl -u ogscope -b --no-pager | grep -i -E 'camera|libcamera|imx327' ``` -## 📚 下一步 - -- 阅读 [用户手册](./user_guide/user-manual.md) -- 查看 [开发文档](./development/README.md) -- 开始 [硬件组装](./hardware/assembly-guide.md) - -## 🆘 获取帮助 +确认相机 overlay、排线方向和 `/boot/firmware/config.txt` 后再重启。 -- [GitHub Issues](https://github.com/your-username/OGScope/issues) -- [GitHub Discussions](https://github.com/your-username/OGScope/discussions) -- [查看文档](./README.md) +### 网络或热点问题 ---- +以 [WiFi / NetworkManager 指南](development/wifi-nm.md)为准,不要同时使用多套网络初始化脚本。 -祝你使用愉快!🎉 +## 8. 后续文档 +- [文档索引](README.md) | [English](README_EN.md) +- [开发指南](development/README.md) | [English](development/README_EN.md) +- [Core REST v1 契约](contracts/core-rest-v1.md) | [English](contracts/core-rest-v1_EN.md) +- [调试控制台](DEBUG_CONSOLE.md) | [English](DEBUG_CONSOLE_EN.md) +- [问题反馈](https://github.com/OG-star-tech/OGScope/issues) diff --git a/docs/QUICK_START_EN.md b/docs/QUICK_START_EN.md index 451f27c..fb62a99 100644 --- a/docs/QUICK_START_EN.md +++ b/docs/QUICK_START_EN.md @@ -1,158 +1,147 @@ # OGScope Quick Start Guide -This guide will help you quickly set up the OGScope development environment. - English | [中文](QUICK_START.md) -## 🎯 Goals - -- ✅ Run OGScope on Raspberry Pi Zero 2W -- ✅ Configure PyCharm Professional remote development -- ✅ Access the system through web interface +Use this guide for a first installation and basic validation on a Raspberry Pi Zero 2W. See the [Development Guide](development/README_EN.md) for all installer options, WiFi, low-memory tuning, and routine updates. -## 📋 Prerequisites +## 1. Prepare -### Hardware Requirements +### Hardware -- Raspberry Pi Zero 2W development board -- IMX327 camera module -- 2.4" SPI LCD display -- MicroSD card (32GB+) -- Power supply (5V/2A) +- Raspberry Pi Zero 2W +- IMX327 MIPI camera +- 32 GB or larger MicroSD card +- Stable 5 V / 2 A or better power supply -### Software Requirements +### Software -- macOS/Windows/Linux development machine -- PyCharm Professional 2025 -- Python 3.9+ -- Poetry package manager +- Raspberry Pi OS Lite 64-bit (Debian/apt based) +- Python 3.10+ +- Git +- Access to apt, PyPI, and GitHub during first installation -## 🚀 Installation Steps +Use Raspberry Pi Imager to configure the hostname, SSH, public key, and initial user. Do not rely on historical default usernames or passwords. -### Step 1: Prepare Raspberry Pi Zero 2W +## 2. Connect to the board -1. **Flash the OS** - ```bash - # Download Raspberry Pi OS image for Raspberry Pi Zero 2W - # Flash to microSD card using balenaEtcher - ``` +```bash +ssh @raspberrypi.local +# Use the router-assigned IP when mDNS is unavailable +ssh @ +``` -2. **Initial Setup** - ```bash - # Boot the board and connect via SSH - ssh pi@orangepi.local - - # Update system - sudo apt update && sudo apt upgrade -y - - # Install essential packages - sudo apt install -y python3.9 python3-pip python3-venv git - ``` +Public-key authentication is recommended: -3. **Install Poetry** - ```bash - curl -sSL https://install.python-poetry.org | python3 - - echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc - source ~/.bashrc - ``` +```bash +ssh-copy-id @ +ssh -o BatchMode=yes @ true +``` -### Step 2: Clone and Setup Project +## 3. Clone and install ```bash -# Clone the repository git clone https://github.com/OG-star-tech/OGScope.git cd OGScope +chmod +x scripts/bootstrap.sh +./scripts/bootstrap.sh +``` -# Install dependencies -poetry install +`bootstrap.sh` deploys the runtime tree to `/opt/ogscope` by default, then creates the project virtual environment, installs system dependencies, and registers `ogscope.service`. + +For mainland China mirrors: + +```bash +export OGSCOPE_MIRROR=cn +./scripts/bootstrap.sh +``` + +For the minimal runtime: + +```bash +OGSCOPE_BOOTSTRAP_MODE=min ./scripts/bootstrap.sh +``` + +When the source already resides in the target runtime directory, you can run: -# Activate virtual environment -poetry shell +```bash +./scripts/install.sh ``` -### Step 3: Configure PyCharm +## 4. Verify -1. **Open Project** - - Launch PyCharm Professional - - Open the OGScope project directory +```bash +sudo systemctl status ogscope --no-pager +curl -fsS http://127.0.0.1:8000/health +curl -fsS http://127.0.0.1:8000/api/core/v1/system/status +``` -2. **Configure File Sync** - - Go to `Tools` → `Deployment` → `Configuration` - - Add SFTP server for Raspberry Pi Zero 2W - - Configure automatic file synchronization +Open: -3. **Setup Run Configurations** - - Create local run configuration for development - - Create remote run configuration for hardware testing +- Home: `http://:8000/` +- Debug console: `http://:8000/debug` +- API documentation: `http://:8000/docs` -### Step 4: Run the Application +Camera checks: ```bash -# Local development -python -m ogscope.main +rpicam-hello --list-cameras 2>/dev/null || libcamera-hello --list-cameras +curl -fsS http://127.0.0.1:8000/api/core/v1/camera/status +``` + +`POST /api/core/v1/camera/start` succeeds only after the camera confirms both `connected=true` and `streaming=true`. -# Remote testing (on Raspberry Pi) -ssh orangepi -cd /home/pi/OGScope -poetry run python -m ogscope.main +## 5. Routine updates + +```bash +cd /opt/ogscope +./scripts/board-update.sh ``` -## 🌐 Access Web Interface +To install development dependencies: -After starting the application, access: -- Local: http://localhost:8000 -- Remote: http://orangepi.local:8000 +```bash +OGSCOPE_INSTALL_DEV=1 ./scripts/board-update.sh +``` -## 🔧 Development Workflow +## 6. Local development -1. **Local Development** - - Write code in PyCharm - - Test basic functionality locally - - Use local run configuration +```bash +poetry install +poetry run pytest -q + +cd web/spa +npm ci +npm run build +``` -2. **File Synchronization** - - Files automatically sync to Raspberry Pi - - Manual sync when needed +Changes involving the camera, GPIO, I2C, or systemd must be revalidated on real hardware. See the [Development Guide](development/README_EN.md) and `scripts/sync_dev_board.sh` for board synchronization. -3. **Hardware Testing** - - Switch to remote run configuration - - Test camera and hardware features - - Debug on actual hardware +## 7. Troubleshooting -## 📚 Next Steps +### Service does not start -- Read [Development Guide](development/README.md) -- Follow the remote workflow in [Development Guide](development/README.md) -- Explore [API Documentation](API_ARCHITECTURE.md) +```bash +sudo journalctl -u ogscope -b --no-pager -n 200 +sudo systemctl cat ogscope +``` -## 🆘 Troubleshooting +### Camera is unavailable -### Common Issues +```bash +rpicam-hello --list-cameras 2>/dev/null || libcamera-hello --list-cameras +sudo journalctl -u ogscope -b --no-pager | grep -i -E 'camera|libcamera|imx327' +``` -1. **Connection Problems** - ```bash - # Check network connectivity - ping orangepi.local - - # Verify SSH connection - ssh orangepi - ``` +Check the camera overlay, ribbon orientation, and `/boot/firmware/config.txt` before rebooting. -2. **Permission Issues** - ```bash - # Fix camera permissions - sudo usermod -a -G video pi - sudo reboot - ``` +### Network or hotspot problems -3. **Dependency Issues** - ```bash - # Reinstall dependencies - poetry install --sync - ``` +Follow the [WiFi / NetworkManager Guide](development/wifi-nm_EN.md). Do not run multiple network initialization stacks at the same time. -## 📞 Support +## 8. Next documents -- [GitHub Issues](https://github.com/OG-star-tech/OGScope/issues) -- [Discussions](https://github.com/OG-star-tech/OGScope/discussions) -- [Documentation](README.md) +- [Documentation index](README_EN.md) | [中文](README.md) +- [Development Guide](development/README_EN.md) | [中文](development/README.md) +- [Core REST v1 contract](contracts/core-rest-v1_EN.md) | [中文](contracts/core-rest-v1.md) +- [Debug console](DEBUG_CONSOLE_EN.md) | [中文](DEBUG_CONSOLE.md) +- [Issue tracker](https://github.com/OG-star-tech/OGScope/issues) diff --git a/docs/README.md b/docs/README.md index 76dd781..c5c44c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,5 +40,6 @@ ### E 工具与贡献 +- [脚本编写与调用规范](development/SCRIPT_STANDARDS.md) | [English](development/SCRIPT_STANDARDS_EN.md) - [调试控制台](DEBUG_CONSOLE.md) | [English](DEBUG_CONSOLE_EN.md) - [贡献指南](../CONTRIBUTING.md) | [English](../CONTRIBUTING_EN.md) diff --git a/docs/README_EN.md b/docs/README_EN.md index cadf513..73daa52 100644 --- a/docs/README_EN.md +++ b/docs/README_EN.md @@ -40,5 +40,6 @@ English | [中文](README.md) ### E Tools and contributing +- [Script authoring and invocation standard](development/SCRIPT_STANDARDS_EN.md) | [中文](development/SCRIPT_STANDARDS.md) - [Debug console](DEBUG_CONSOLE_EN.md) | [中文](DEBUG_CONSOLE.md) - [Contributing](../CONTRIBUTING_EN.md) | [中文](../CONTRIBUTING.md) diff --git a/docs/contracts/core-rest-v1.md b/docs/contracts/core-rest-v1.md index 8677dc6..4aaf987 100644 --- a/docs/contracts/core-rest-v1.md +++ b/docs/contracts/core-rest-v1.md @@ -70,6 +70,8 @@ - 返回相机连接状态、流状态、runtime overrides 与可选 `ambient_hint` - `ambient_hint` 是环境亮度建议遥测,供上层设备做显示/交互策略参考;典型字段包括 `available`、`dark_score`(0.0 明亮到 1.0 昏暗)、`lux`、`exposure_us`、`digital_gain` - `POST /api/core/v1/camera/start` + - 仅当相机启动命令成功且状态确认 `connected=true`、`streaming=true` 时返回 `success=true` + - `applied` 包含 `action`、`hardware_plane_ok`、`ready`、`connected`、`streaming`;调用方应以 `ready` 判断是否可立即取帧 - `POST /api/core/v1/camera/stop` MJPEG 连续视频流与流控状态、单帧 JPEG 预览(轮询、`since_frame_id`、调试限频)**仅**暴露于开发路径(不再在 `/api/core/v1/` 重复): diff --git a/docs/contracts/core-rest-v1_EN.md b/docs/contracts/core-rest-v1_EN.md index cacae6b..be3732b 100644 --- a/docs/contracts/core-rest-v1_EN.md +++ b/docs/contracts/core-rest-v1_EN.md @@ -69,6 +69,8 @@ This document defines the **minimal stable REST surface** for callers integratin - `GET /api/core/v1/camera/status` — connection, stream state, runtime overrides, and optional `ambient_hint` - `ambient_hint` is advisory ambient-light telemetry for upstream display/interaction policy. Typical fields: `available`, `dark_score` (0.0 bright to 1.0 dark), `lux`, `exposure_us`, `digital_gain` - `POST /api/core/v1/camera/start` + - Returns `success=true` only when the start command succeeds and status confirms both `connected=true` and `streaming=true` + - `applied` includes `action`, `hardware_plane_ok`, `ready`, `connected`, and `streaming`; callers should use `ready` before requesting frames - `POST /api/core/v1/camera/stop` MJPEG stream, stream control status, and single-frame JPEG preview (polling, `since_frame_id`, debug rate limits) are **only** on developer paths (not duplicated under `/api/core/v1/`): diff --git a/docs/development/README.md b/docs/development/README.md index e865e04..229d556 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -12,7 +12,7 @@ - **B 板上运维**:[WiFi](wifi-nm.md) | [English](wifi-nm_EN.md);[星库](plate-solve-data.md);[稳定性](ogscope-service-hardening.md);[BOM](../hardware/bom.md) | [English](../hardware/bom_EN.md) - **C API/契约/测试**:[API 架构(含 FastAPI 入口)](../API_ARCHITECTURE.md) | [English](../API_ARCHITECTURE_EN.md);[系统架构](../architecture/OGSCOPE_SYSTEM_ARCHITECTURE_BILINGUAL.md);[Core 契约](../contracts/core-rest-v1.md) | [English](../contracts/core-rest-v1_EN.md);[Dev 契约](../contracts/dev-rest-v1.md) | [English](../contracts/dev-rest-v1_EN.md);[兼容矩阵](../contracts/core-compatibility-matrix.md);[自检](ARCHITECTURE_QUICK_CHECKLIST.md) | [English](ARCHITECTURE_QUICK_CHECKLIST_EN.md);[测试](testing-guide.md) | [English](testing-guide_EN.md) - **D 外部集成**:[Subordinate 模式](../contracts/subordinate-mode.md) | [English](../contracts/subordinate-mode_EN.md);[Hardware Plane UDS v1](../contracts/hardware-plane-uds-v1.md) | [English](../contracts/hardware-plane-uds-v1_EN.md) -- **E 工具与贡献**:[脚本规范](SCRIPT_STANDARDS.md);[调试控制台](../DEBUG_CONSOLE.md) | [English](../DEBUG_CONSOLE_EN.md);[贡献指南](../../CONTRIBUTING.md) | [English](../../CONTRIBUTING_EN.md) +- **E 工具与贡献**:[脚本规范](SCRIPT_STANDARDS.md) | [English](SCRIPT_STANDARDS_EN.md);[调试控制台](../DEBUG_CONSOLE.md) | [English](../DEBUG_CONSOLE_EN.md);[贡献指南](../../CONTRIBUTING.md) | [English](../../CONTRIBUTING_EN.md) 当前推荐流程为:**本地编辑代码 -> 上传到开发板 -> 使用 `systemd` 重启服务验证**。 该流程与实际硬件运行环境一致,适合涉及相机与系统库依赖的场景。 diff --git a/docs/development/README_EN.md b/docs/development/README_EN.md index 17e92e5..680ee84 100644 --- a/docs/development/README_EN.md +++ b/docs/development/README_EN.md @@ -14,7 +14,7 @@ and the team-standard debug workflow. - **B Board ops**: [WiFi](wifi-nm_EN.md) | [中文](wifi-nm.md); [plate-solve data](plate-solve-data.md); [stability / memory](ogscope-service-hardening.md); [BOM](../hardware/bom_EN.md) | [中文](../hardware/bom.md) - **C API and quality**: [API architecture (includes FastAPI entry)](../API_ARCHITECTURE_EN.md) | [中文](../API_ARCHITECTURE.md); [system architecture](../architecture/OGSCOPE_SYSTEM_ARCHITECTURE_BILINGUAL.md); [Core contract](../contracts/core-rest-v1_EN.md) | [中文](../contracts/core-rest-v1.md); [Dev contract](../contracts/dev-rest-v1_EN.md) | [中文](../contracts/dev-rest-v1.md); [compatibility matrix](../contracts/core-compatibility-matrix.md); [checklist](ARCHITECTURE_QUICK_CHECKLIST_EN.md) | [中文](ARCHITECTURE_QUICK_CHECKLIST.md); [testing](testing-guide_EN.md) | [中文](testing-guide.md) - **D External integration**: [Subordinate mode](../contracts/subordinate-mode_EN.md) | [中文](../contracts/subordinate-mode.md); [Hardware Plane UDS v1](../contracts/hardware-plane-uds-v1_EN.md) | [中文](../contracts/hardware-plane-uds-v1.md) -- **E Tools and contributing**: [Debug console](../DEBUG_CONSOLE_EN.md) | [中文](../DEBUG_CONSOLE.md); [Contributing](../../CONTRIBUTING_EN.md) | [中文](../../CONTRIBUTING.md) +- **E Tools and contributing**: [Script standard](SCRIPT_STANDARDS_EN.md) | [中文](SCRIPT_STANDARDS.md); [Debug console](../DEBUG_CONSOLE_EN.md) | [中文](../DEBUG_CONSOLE.md); [Contributing](../../CONTRIBUTING_EN.md) | [中文](../../CONTRIBUTING.md) Recommended workflow: **edit locally -> upload to board -> restart with `systemd` -> verify**. diff --git a/docs/development/SCRIPT_STANDARDS.md b/docs/development/SCRIPT_STANDARDS.md index 79d7c34..db4cb92 100644 --- a/docs/development/SCRIPT_STANDARDS.md +++ b/docs/development/SCRIPT_STANDARDS.md @@ -1,6 +1,6 @@ # OGScope 脚本编写与调用规范 -中文 | English (inline) +中文 | [English](SCRIPT_STANDARDS_EN.md) 本规范用于统一 `scripts/` 与后续 `deploy/` 相关脚本的行为,降低双仓联动时的维护成本与误操作风险。 @@ -98,4 +98,3 @@ - 涉及运行路径改动时,验证 `systemd` 与健康检查可用: - `systemctl status ogscope` - `curl -s http://127.0.0.1:8000/health` - diff --git a/docs/development/SCRIPT_STANDARDS_EN.md b/docs/development/SCRIPT_STANDARDS_EN.md new file mode 100644 index 0000000..85040e3 --- /dev/null +++ b/docs/development/SCRIPT_STANDARDS_EN.md @@ -0,0 +1,93 @@ +# OGScope Script Authoring and Invocation Standard + +English | [中文](SCRIPT_STANDARDS.md) + +This standard keeps `scripts/` and future `deploy/` helpers consistent, reducing maintenance cost and operational risk for external integrations. + +## 1. Scope and boundaries + +- OGScope scripts manage only OGScope installation, updates, removal, diagnostics, and repair. +- Do not embed deployment logic for a specific downstream product. +- External integrators own combined-stack orchestration; OGScope exposes stable script entry points and health checks. + +## 2. Unified command model + +Recommended future entry point: + +```bash +./scripts/stackctl.sh [options] +``` + +Standard commands: + +- `install`: first installation, including minimal and extended modes +- `update`: incremental update +- `uninstall`: removal +- `doctor`: read-only diagnostics +- `repair`: allowlisted repairs +- `status`: current runtime and version summary + +## 3. Arguments + +Scripts should support these common arguments where applicable: + +- `--yes`: non-interactive confirmation for CI and automation +- `--dry-run`: inspect without writing files or restarting services +- `--json`: machine-readable output +- `--verbose`: detailed progress logs +- `--strict`: treat warnings as failures + +Invalid arguments must return the argument-error exit code. Destructive actions require explicit confirmation unless `--yes` is supplied. + +## 4. Exit codes + +- `0`: success +- `2`: invalid arguments +- `3`: preconditions not met, such as dependencies, permissions, or environment +- `4`: execution failed +- `5`: partial success with incomplete actions + +## 5. Idempotency and safety + +- Scripts must be idempotent and must not damage an already working installation when rerun. +- Changes to `systemd`, `/etc/*`, or networking must identify the target, support a dry run, and provide a recovery path on failure. +- Never print passwords, tokens, private keys, or other secrets to logs. + +## 6. Output and logging + +- Human-readable critical messages should be bilingual. +- JSON output must include at least `success`, `code`, `summary`, and `checks` or `actions`. +- Use stable `INFO`, `WARN`, and `ERROR` prefixes for important steps. + +## 7. Files and naming + +- Use `kebab-case` script names. +- Put entry scripts in `scripts/` and reusable functions in `scripts/lib/`. +- Prefer shared functions over duplicated system operations. + +## 8. Privilege handling + +- Check privileges when the top-level script starts; do not elevate unexpectedly in the middle of a workflow. +- Group root operations and explain them before execution. +- Handle `sudo -n` failures with a clear message and a manual recovery command. + +## 9. External integration + +OGScope remains a core capability provider. It guarantees: + +- stable install, update, and uninstall entry points; +- diagnosable health and configuration state; +- the `core/v1` and [subordinate mode](../contracts/subordinate-mode_EN.md) contracts. + +Combined deployment and product orchestration belong to the external integrator. + +## 10. Minimum checks before committing script changes + +- Run `bash -n` for every changed shell script. +- Run the target script with `--help` or an equivalent argument check. +- When runtime paths change, validate: + +```bash +systemctl status ogscope +curl -s http://127.0.0.1:8000/health +``` diff --git a/ogscope/core/application/core_service.py b/ogscope/core/application/core_service.py index f4c97f8..4b42201 100644 --- a/ogscope/core/application/core_service.py +++ b/ogscope/core/application/core_service.py @@ -333,7 +333,9 @@ async def start_camera(self) -> dict[str, Any]: ready = bool(status.get("connected")) and bool(status.get("streaming")) message = str(result.get("message", "")) if not ready: - message = str(status.get("error") or message or "camera did not become ready") + message = str( + status.get("error") or message or "camera did not become ready" + ) return { "success": bool(result.get("success", True)) and ready, "message": message, diff --git a/ogscope/web/api/models/schemas.py b/ogscope/web/api/models/schemas.py index 9295ad9..d75756b 100644 --- a/ogscope/web/api/models/schemas.py +++ b/ogscope/web/api/models/schemas.py @@ -231,7 +231,7 @@ class SolveContextQuality(BaseModel): class SolveContextPayload(BaseModel): - """Optional sensor context from ZenitAPA / ZenitAPA 提供的可选传感器上下文。""" + """外部系统提供的可选传感器上下文 / Optional external sensor context.""" model_config = ConfigDict(extra="forbid") diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index 86cf914..9560be9 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -82,6 +82,48 @@ def test_core_camera_ambient_hint_from_metadata() -> None: assert hint["exposure_us"] == 80_000 +@pytest.mark.unit +@pytest.mark.asyncio +async def test_core_camera_start_requires_confirmed_ready(monkeypatch) -> None: + """启动成功仍须确认真实流就绪 / A successful command still requires stream readiness.""" + from ogscope.core.application import core_service + + class _HardwareClient: + async def device_command(self, service: str, command: str) -> dict: + assert (service, command) == ("camera", "start") + return {"success": True} + + async def _start() -> dict: + return {"success": True, "message": "start accepted"} + + async def _not_ready() -> dict: + return { + "success": True, + "connected": True, + "streaming": False, + "info": {"model": "IMX327"}, + "error": "camera did not report a valid stream", + } + + service = core_service.CoreContractService() + monkeypatch.setattr(core_service, "get_hardware_plane_client", _HardwareClient) + monkeypatch.setattr(core_service.camera_domain_service, "start", _start) + monkeypatch.setattr(service, "get_camera_status", _not_ready) + + result = await service.start_camera() + + assert result["success"] is False + assert result["info"]["model"] == "IMX327" + assert result["applied"] == { + "action": "start", + "hardware_plane_ok": True, + "ready": False, + "connected": True, + "streaming": False, + } + assert result["message"] == "camera did not report a valid stream" + + @pytest.mark.unit def test_core_system_status_network_delegated_when_subordinate(monkeypatch) -> None: """subordinate 下 network 标记 delegated 且不降级 / Subordinate marks network delegated.""" From 53c4b81a7b04955e6369009229f418f47fffb9ff 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: Wed, 12 Aug 2026 20:43:54 +0800 Subject: [PATCH 03/24] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Core=20?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E9=A2=84=E8=A7=88=E5=A5=91=E7=BA=A6=20/=20ad?= =?UTF-8?q?d=20Core=20camera=20preview=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contracts/core-rest-v1.md | 8 ++++-- docs/contracts/core-rest-v1_EN.md | 8 ++++-- ogscope/domain/camera/streaming.py | 4 +++ ogscope/web/api/core/routes.py | 39 +++++++++++++++++++++++++++- tests/unit/test_core_contract_api.py | 26 +++++++++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/contracts/core-rest-v1.md b/docs/contracts/core-rest-v1.md index 4aaf987..4b0155d 100644 --- a/docs/contracts/core-rest-v1.md +++ b/docs/contracts/core-rest-v1.md @@ -73,10 +73,14 @@ - 仅当相机启动命令成功且状态确认 `connected=true`、`streaming=true` 时返回 `success=true` - `applied` 包含 `action`、`hardware_plane_ok`、`ready`、`connected`、`streaming`;调用方应以 `ready` 判断是否可立即取帧 - `POST /api/core/v1/camera/stop` +- `GET /api/core/v1/camera/preview/stream?quality=75` + - 产品级 MJPEG 连续预览;使用与相机分析共享的预览消费者和并发限制 + - `quality` 范围为 `10`–`100`;省略时使用服务端预览质量配置 + - 响应禁止缓存;达到并发上限时返回 `503` -MJPEG 连续视频流与流控状态、单帧 JPEG 预览(轮询、`since_frame_id`、调试限频)**仅**暴露于开发路径(不再在 `/api/core/v1/` 重复): +流控诊断状态和单帧 JPEG 预览(轮询、`since_frame_id`、调试限频)仍仅暴露于开发路径: -- `GET /api/dev/debug/camera/stream?quality=75` — MJPEG 压缩流(JPEG) +- `GET /api/dev/debug/camera/stream?quality=75` — 与 Core 预览共享实现的开发入口 - `GET /api/dev/debug/camera/stream/status` — `max_clients`、`active_clients`、取帧超时、目标预览帧率 - `GET /api/dev/debug/camera/preview` — 单帧预览 diff --git a/docs/contracts/core-rest-v1_EN.md b/docs/contracts/core-rest-v1_EN.md index be3732b..5654143 100644 --- a/docs/contracts/core-rest-v1_EN.md +++ b/docs/contracts/core-rest-v1_EN.md @@ -72,10 +72,14 @@ This document defines the **minimal stable REST surface** for callers integratin - Returns `success=true` only when the start command succeeds and status confirms both `connected=true` and `streaming=true` - `applied` includes `action`, `hardware_plane_ok`, `ready`, `connected`, and `streaming`; callers should use `ready` before requesting frames - `POST /api/core/v1/camera/stop` +- `GET /api/core/v1/camera/preview/stream?quality=75` + - Product MJPEG preview using the shared preview consumer and concurrency limiter + - `quality` ranges from `10` to `100`; omission uses the server preview-quality setting + - Responses are non-cacheable; the endpoint returns `503` when the client limit is reached -MJPEG stream, stream control status, and single-frame JPEG preview (polling, `since_frame_id`, debug rate limits) are **only** on developer paths (not duplicated under `/api/core/v1/`): +Stream diagnostics and single-frame JPEG preview (polling, `since_frame_id`, debug rate limits) remain developer-only: -- `GET /api/dev/debug/camera/stream?quality=75` — MJPEG (JPEG) +- `GET /api/dev/debug/camera/stream?quality=75` — developer entry backed by the shared Core preview implementation - `GET /api/dev/debug/camera/stream/status` — `max_clients`, `active_clients`, grab timeout, target preview FPS - `GET /api/dev/debug/camera/preview` — single-frame preview diff --git a/ogscope/domain/camera/streaming.py b/ogscope/domain/camera/streaming.py index dfe5df4..28036d3 100644 --- a/ogscope/domain/camera/streaming.py +++ b/ogscope/domain/camera/streaming.py @@ -98,4 +98,8 @@ async def frame_generator(): return StreamingResponse( frame_generator(), media_type=f"multipart/x-mixed-replace; boundary={boundary}", + headers={ + "Cache-Control": "no-store, no-cache, must-revalidate", + "X-Accel-Buffering": "no", + }, ) diff --git a/ogscope/web/api/core/routes.py b/ogscope/web/api/core/routes.py index 53152d6..b6f888c 100644 --- a/ogscope/web/api/core/routes.py +++ b/ogscope/web/api/core/routes.py @@ -2,9 +2,13 @@ Core v1 标准契约路由 / Core v1 standard contract routes. """ -from fastapi import APIRouter, HTTPException +import logging + +from fastapi import APIRouter, HTTPException, Query, Request +from fastapi.responses import StreamingResponse from ogscope.core.application import core_contract_service +from ogscope.domain.camera.streaming import build_camera_mjpeg_stream from ogscope.domain.shared.filesystem import ensure_safe_basename from ogscope.web.api.models.schemas import ( CoreAnalysisControlResponse, @@ -19,6 +23,12 @@ ) router = APIRouter() +logger = logging.getLogger(__name__) + +_MJPEG_LIMIT_DETAIL = ( + "MJPEG stream limit reached; close other previews or tabs / " + "已达到 MJPEG 同时连接上限,请关闭其他标签页的预览" +) @router.post( @@ -92,6 +102,33 @@ async def core_camera_status() -> CoreCameraStatusResponse: raise HTTPException(status_code=500, detail=str(exc)) from exc +@router.get("/core/v1/camera/preview/stream") +async def core_camera_preview_stream( + request: Request, + quality: int | None = Query(None, ge=10, le=100), +) -> StreamingResponse: + """产品级 MJPEG 相机预览 / Product MJPEG camera preview.""" + try: + from ogscope.config import get_settings + + effective_quality = int(quality or get_settings().preview_jpeg_quality) + return await build_camera_mjpeg_stream( + request, + image_format="jpeg", + quality=effective_quality, + limit_detail=_MJPEG_LIMIT_DETAIL, + timeout_log_message=( + "Core MJPEG 单帧取流超时,结束响应以释放名额 / " + "Core MJPEG frame fetch timed out, closing stream" + ), + logger=logger, + ) + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @router.post("/core/v1/camera/start", response_model=CoreCameraControlResponse) async def core_camera_start() -> CoreCameraControlResponse: """启动相机(Core 标准契约)/ Start camera (Core contract).""" diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index 9560be9..99be413 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -294,6 +294,31 @@ def test_core_video_info_rejects_invalid_filename(client) -> None: assert resp.json()["detail"] == "invalid filename" +@pytest.mark.unit +def test_core_camera_preview_stream_is_product_contract(client, monkeypatch) -> None: + """Core 预览应暴露稳定 MJPEG 契约 / Core preview exposes stable MJPEG contract.""" + from fastapi.responses import StreamingResponse + + from ogscope.web.api.core import routes as core_routes + + async def _fake_stream(*_args, **_kwargs): + async def _frames(): + yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\ntest\r\n" + + return StreamingResponse( + _frames(), + media_type="multipart/x-mixed-replace; boundary=frame", + ) + + monkeypatch.setattr(core_routes, "build_camera_mjpeg_stream", _fake_stream) + + resp = client.get("/api/core/v1/camera/preview/stream?quality=75") + + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("multipart/x-mixed-replace") + assert b"Content-Type: image/jpeg" in resp.content + + @pytest.mark.unit def test_docs_are_split_between_core_and_dev(client) -> None: """文档默认 core,dev 单独入口 / Docs split into core default and dev page.""" @@ -330,6 +355,7 @@ def test_core_openapi_contains_required_business_endpoints(client) -> None: "/api/core/v1/analysis/result", "/api/core/v1/analysis/stop", "/api/core/v1/camera/status", + "/api/core/v1/camera/preview/stream", } assert required.issubset(paths) assert all(path.startswith("/api/core/v1/") for path in paths) From f5757f541809cd932c1c5363853b4393ceec832c 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: Wed, 12 Aug 2026 20:55:26 +0800 Subject: [PATCH 04/24] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=B7=A5=E5=85=B7=E5=8F=B0=E5=B0=8F=E5=B1=8F=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E4=B8=8E=E6=9C=AC=E5=9C=B0=E5=8C=96=20/=20improve=20d?= =?UTF-8?q?eveloper=20tools=20responsiveness=20and=20localization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/spa/src/apps/camera/CameraConsoleApp.tsx | 25 +-- web/spa/src/apps/lab/AnalysisLabApp.tsx | 71 +++++++- web/spa/src/apps/system/DebugShell.tsx | 64 +++++-- web/spa/src/apps/system/pages/NetworkPage.tsx | 161 +++++++++++------- web/spa/src/apps/system/pages/SensorsPage.tsx | 23 ++- .../analysis-lab/assets/analysis-D_aCLSNL.js | 51 ------ .../analysis-lab/assets/analysis-Dd7iQ8pr.js | 51 ++++++ .../analysis-lab/assets/camera-aU1xxh-S.js | 56 ++++++ .../analysis-lab/assets/camera-hzdNY7z2.js | 61 ------- .../analysis-lab/assets/http-VZMNcsmS.js | 11 -- .../analysis-lab/assets/http-lbh6zBy9.js | 16 ++ .../analysis-lab/assets/index-BtAa6NQk.css | 1 + .../analysis-lab/assets/index-C78KOEFu.js | 26 --- .../analysis-lab/assets/index-CV2LaaT8.js | 26 +++ .../analysis-lab/assets/index-DstXdMlD.css | 1 - ...-cw-gseCLMRP.js => refresh-cw-CIJtimxg.js} | 2 +- .../analysis-lab/assets/system-BMLXYbaG.js | 83 +++++++++ .../analysis-lab/assets/system-Z3O5uO1l.js | 78 --------- web/static/analysis-lab/camera.html | 8 +- web/static/analysis-lab/index.html | 8 +- web/static/analysis-lab/system.html | 10 +- web/static/i18n/analysis.en.json | 1 + web/static/i18n/analysis.zh.json | 1 + 23 files changed, 492 insertions(+), 343 deletions(-) delete mode 100644 web/static/analysis-lab/assets/analysis-D_aCLSNL.js create mode 100644 web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js create mode 100644 web/static/analysis-lab/assets/camera-aU1xxh-S.js delete mode 100644 web/static/analysis-lab/assets/camera-hzdNY7z2.js delete mode 100644 web/static/analysis-lab/assets/http-VZMNcsmS.js create mode 100644 web/static/analysis-lab/assets/http-lbh6zBy9.js create mode 100644 web/static/analysis-lab/assets/index-BtAa6NQk.css delete mode 100644 web/static/analysis-lab/assets/index-C78KOEFu.js create mode 100644 web/static/analysis-lab/assets/index-CV2LaaT8.js delete mode 100644 web/static/analysis-lab/assets/index-DstXdMlD.css rename web/static/analysis-lab/assets/{refresh-cw-gseCLMRP.js => refresh-cw-CIJtimxg.js} (91%) create mode 100644 web/static/analysis-lab/assets/system-BMLXYbaG.js delete mode 100644 web/static/analysis-lab/assets/system-Z3O5uO1l.js diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index 0a1b3c3..62bdfbc 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -609,6 +609,7 @@ export function CameraConsoleApp() { }; const resetSettings = async () => { + if (!window.confirm(t("cam.confirm.reset"))) return; setErr(null); try { await requestJson("/api/debug/camera/reset", { method: "POST" }); @@ -1085,9 +1086,9 @@ export function CameraConsoleApp() { return (
-
-
-

{`OGScope ${t("cam.title")}`}

+
+
+

{`OGScope ${t("cam.title")}`}

+ +
+ )} + + {(assetDrawerOpen || parameterDrawerOpen) && ( + +
{t("sidebar.assets")}
-
+
-

{t("sys.shell.workbench")}

+

{t("sys.shell.workbench")}

{t("sys.shell.node")}: OGSCOPE_PI_ZERO_2W

@@ -135,14 +161,24 @@ export function DebugShell({
-
-
+
+
+ {routeTitle[route]}
-
+
-
{children}
+
{children}
); diff --git a/web/spa/src/apps/system/pages/NetworkPage.tsx b/web/spa/src/apps/system/pages/NetworkPage.tsx index cb4298a..d8ee5dd 100644 --- a/web/spa/src/apps/system/pages/NetworkPage.tsx +++ b/web/spa/src/apps/system/pages/NetworkPage.tsx @@ -1,20 +1,21 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { RefreshCw, Search, Wifi } from "lucide-react"; import { useSystemInfo } from "@shared/context/SystemInfoContext"; +import { useI18n } from "@shared/i18n/I18nProvider"; import { requestJson } from "@shared/transport/http"; function httpPort(): number { return typeof window.OGSCOPE_HTTP_PORT === "number" ? window.OGSCOPE_HTTP_PORT : 8000; } -function formatUptime(sec: unknown): string { +function formatUptime(sec: unknown, isZh: boolean): string { const s = Math.max(0, parseInt(String(sec ?? 0), 10) || 0); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); - if (d > 0) return `${d}天 ${h}小时`; - if (h > 0) return `${h}小时 ${m}分`; - return `${m}分`; + if (d > 0) return isZh ? `${d}天 ${h}小时` : `${d}d ${h}h`; + if (h > 0) return isZh ? `${h}小时 ${m}分` : `${h}h ${m}m`; + return isZh ? `${m}分` : `${m}m`; } type WifiPayload = { @@ -34,8 +35,11 @@ type ScanNetwork = { ssid: string; signal?: string | number; security?: string } type Profile = { connection_name: string; ssid: string; autoconnect?: boolean }; export function NetworkPage() { + const { locale } = useI18n(); + const isZh = locale === "zh"; + const tr = useCallback((zh: string, en: string) => (isZh ? zh : en), [isZh]); const { info, error: sysErr } = useSystemInfo(); - const [wifiText, setWifiText] = useState("加载中..."); + const [wifiText, setWifiText] = useState(() => tr("加载中...", "Loading...")); const [manualHint, setManualHint] = useState(""); const [scanStatus, setScanStatus] = useState(""); const [networks, setNetworks] = useState([]); @@ -50,14 +54,15 @@ export function NetworkPage() { const [wizardSsid, setWizardSsid] = useState("OGScope_xxxx"); const [mdnsHref, setMdnsHref] = useState(null); const [mdnsLabel, setMdnsLabel] = useState("—"); + const [showWizardPassword, setShowWizardPassword] = useState(false); const renderWifi = useCallback((data: WifiPayload) => { const mode = data.mode || "unknown"; const active = data.active_connection || "-"; const iface = data.wireless_interface || "wlan0"; const apIpv4 = data.ap_ipv4 || "-"; - const configured = data.configured ? "是" : "否"; - const message = data.message ? `,消息: ${data.message}` : ""; + const configured = data.configured ? tr("是", "Yes") : tr("否", "No"); + const message = data.message ? `${tr(",消息: ", ", message: ")}${data.message}` : ""; if (data.ap_url_hint) setApHint(data.ap_url_hint); if (data.ap_ssid) setWizardSsid(data.ap_ssid); const port = httpPort(); @@ -73,12 +78,12 @@ export function NetworkPage() { setMdnsLabel(href); } else { setMdnsHref(null); - setMdnsLabel("未提供"); + setMdnsLabel(tr("未提供", "Not provided")); } setWifiText( - `模式: ${mode} | 活动连接: ${active} | 接口: ${iface} | AP地址: ${apIpv4} | 已配置: ${configured}${message}`, + `${tr("模式", "Mode")}: ${mode} | ${tr("活动连接", "Active")}: ${active} | ${tr("接口", "Interface")}: ${iface} | ${tr("AP地址", "AP address")}: ${apIpv4} | ${tr("已配置", "Configured")}: ${configured}${message}`, ); - }, []); + }, [tr]); const refreshStatus = useCallback(async () => { const data = await requestJson("/api/network/wifi", { cache: "no-store" }); @@ -86,11 +91,21 @@ export function NetworkPage() { }, [renderWifi]); useEffect(() => { - refreshStatus().catch((err: Error) => setWifiText(`获取状态失败: ${err.message}`)); - }, [refreshStatus]); + refreshStatus().catch((err: Error) => setWifiText(`${tr("获取状态失败", "Status request failed")}: ${err.message}`)); + }, [refreshStatus, tr]); + + useEffect(() => { + setScanStatus(""); + setManualHint(""); + setLanStatus(""); + }, [locale]); const switchMode = async (mode: string) => { - setWifiText(`正在切换到 ${mode.toUpperCase()}...`); + if (!window.confirm(tr( + `切换到 ${mode.toUpperCase()} 可能中断当前连接。确认继续?`, + `Switching to ${mode.toUpperCase()} may disconnect this session. Continue?`, + ))) return; + setWifiText(`${tr("正在切换到", "Switching to")} ${mode.toUpperCase()}...`); const data = await requestJson("/api/network/wifi", { method: "POST", body: JSON.stringify({ mode }), @@ -100,7 +115,7 @@ export function NetworkPage() { const runScan = async () => { setScanBusy(true); - setScanStatus("扫描中..."); + setScanStatus(tr("扫描中...", "Scanning...")); setNetworks([]); try { const res = await requestJson<{ networks?: ScanNetwork[]; hint?: string }>( @@ -110,19 +125,19 @@ export function NetworkPage() { const list = res.networks || []; const hint = res.hint ? ` ${res.hint}` : ""; setNetworks(list); - setScanStatus(`扫描到 ${list.length} 个网络${hint}`.trim()); + setScanStatus(`${tr("扫描到", "Found")} ${list.length} ${tr("个网络", "network(s)")}${hint}`.trim()); } catch (e) { - setScanStatus(`扫描失败: ${e instanceof Error ? e.message : String(e)}`); + setScanStatus(`${tr("扫描失败", "Scan failed")}: ${e instanceof Error ? e.message : String(e)}`); } finally { setScanBusy(false); } }; const connectSsid = async (ssid: string) => { - const pwd = window.prompt(`输入密码: ${ssid}`, ""); + const pwd = window.prompt(`${tr("输入密码", "Enter password")}: ${ssid}`, ""); if (pwd === null) return; setManualHint(""); - setWifiText("正在连接..."); + setWifiText(tr("正在连接...", "Connecting...")); setStaBusy(true); try { const data = await requestJson("/api/network/wifi/sta/connect", { @@ -133,13 +148,13 @@ export function NetworkPage() { const m = data.mode || "unknown"; setManualHint( m === "sta" - ? `连接成功,当前连接: ${data.active_connection || "—"}` - : `连接请求已提交,当前模式: ${m},连接: ${data.active_connection || "—"}`, + ? `${tr("连接成功,当前连接", "Connected")}: ${data.active_connection || "—"}` + : `${tr("连接请求已提交,当前模式", "Connection submitted; mode")}: ${m}, ${tr("连接", "connection")}: ${data.active_connection || "—"}`, ); window.setTimeout(() => void refreshStatus().catch(() => {}), 2500); } catch (e) { - setWifiText(`连接失败: ${e instanceof Error ? e.message : String(e)}`); - setManualHint(`错误详情: ${e instanceof Error ? e.message : String(e)}`); + setWifiText(`${tr("连接失败", "Connection failed")}: ${e instanceof Error ? e.message : String(e)}`); + setManualHint(`${tr("错误详情", "Details")}: ${e instanceof Error ? e.message : String(e)}`); } finally { setStaBusy(false); } @@ -148,11 +163,11 @@ export function NetworkPage() { const connectManual = async () => { const ssid = manualSsid.trim(); if (!ssid) { - window.alert("请输入 SSID"); + window.alert(tr("请输入 SSID", "Enter an SSID")); return; } setManualHint(""); - setWifiText("正在连接..."); + setWifiText(tr("正在连接...", "Connecting...")); setStaBusy(true); try { const data = await requestJson("/api/network/wifi/sta/connect", { @@ -163,13 +178,13 @@ export function NetworkPage() { const m = data.mode || "unknown"; setManualHint( m === "sta" - ? `连接成功,当前连接: ${data.active_connection || "—"}` - : `连接请求已提交,当前模式: ${m},连接: ${data.active_connection || "—"}`, + ? `${tr("连接成功,当前连接", "Connected")}: ${data.active_connection || "—"}` + : `${tr("连接请求已提交,当前模式", "Connection submitted; mode")}: ${m}, ${tr("连接", "connection")}: ${data.active_connection || "—"}`, ); window.setTimeout(() => void refreshStatus().catch(() => {}), 2500); } catch (e) { - setWifiText(`连接失败: ${e instanceof Error ? e.message : String(e)}`); - setManualHint(`错误详情: ${e instanceof Error ? e.message : String(e)}`); + setWifiText(`${tr("连接失败", "Connection failed")}: ${e instanceof Error ? e.message : String(e)}`); + setManualHint(`${tr("错误详情", "Details")}: ${e instanceof Error ? e.message : String(e)}`); } finally { setStaBusy(false); } @@ -196,7 +211,7 @@ export function NetworkPage() { method: "POST", body: JSON.stringify({ connection_name }), }); - setWifiText("已发送激活请求"); + setWifiText(tr("已发送激活请求", "Activation requested")); } catch (e) { window.alert(e instanceof Error ? e.message : String(e)); } @@ -220,7 +235,7 @@ export function NetworkPage() { const scanLan = async () => { const port = httpPort(); - setLanStatus("扫描中..."); + setLanStatus(tr("扫描中...", "Scanning...")); const bases = ["192.168.0", "192.168.1", "192.168.31", "10.0.0"]; for (const base of bases) { const ips: string[] = []; @@ -230,13 +245,13 @@ export function NetworkPage() { const results = await Promise.all(chunk.map((ip) => probeHealth(ip, port))); const hit = results.find(Boolean); if (hit) { - setLanStatus(`已找到设备: ${hit}`); + setLanStatus(`${tr("已找到设备", "Device found")}: ${hit}`); window.location.href = `${hit}/debug`; return; } } } - setLanStatus("未找到设备"); + setLanStatus(tr("未找到设备", "No device found")); }; const sysRows = useMemo(() => { @@ -250,17 +265,17 @@ export function NetworkPage() { ? `${Number(info.wifi_signal_dbm).toFixed(0)} dBm (${String(info.wifi_interface ?? "?")})` : "—"; return [ - ["平台", String(info.platform ?? "—")], - ["系统", String(info.os ?? "—")], - ["CPU 占用", `${Number(info.cpu_usage ?? 0).toFixed(1)}%`], - ["内存占用", `${Number(info.memory_usage ?? 0).toFixed(1)}%`], - ["CPU 温度", `${Number(info.temperature ?? 0).toFixed(1)} °C`], - ["运行时长", formatUptime(info.uptime_seconds)], - ["1 分钟负载", String(info.load_average_1m ?? "—")], - ["WiFi 质量", wifiQ], - ["WiFi 信号", wifiSig], + [tr("平台", "Platform"), String(info.platform ?? "—")], + [tr("系统", "System"), String(info.os ?? "—")], + [tr("CPU 占用", "CPU usage"), info.cpu_usage == null ? "—" : `${Number(info.cpu_usage).toFixed(1)}%`], + [tr("内存占用", "Memory usage"), info.memory_usage == null ? "—" : `${Number(info.memory_usage).toFixed(1)}%`], + [tr("CPU 温度", "CPU temperature"), info.temperature == null ? "—" : `${Number(info.temperature).toFixed(1)} °C`], + [tr("运行时长", "Uptime"), formatUptime(info.uptime_seconds, isZh)], + [tr("1 分钟负载", "Load (1m)"), String(info.load_average_1m ?? "—")], + [tr("WiFi 质量", "WiFi quality"), wifiQ], + [tr("WiFi 信号", "WiFi signal"), wifiSig], ] as [string, string][]; - }, [info]); + }, [info, isZh, tr]); return (
@@ -271,7 +286,9 @@ export function NetworkPage() { System_Network_Debug

NETWORK TERMINAL

-

WiFi 管理、模式切换、发现与恢复工具

+

+ {tr("WiFi 管理、模式切换、发现与恢复工具", "WiFi management, mode switching, discovery, and recovery tools")} +

{sysErr && ( @@ -318,10 +335,10 @@ export function NetworkPage() {

- AP 地址:{apHint} + {tr("AP 地址:", "AP address: ")}{apHint}

- mDNS:{" "} + {tr("mDNS:", "mDNS: ")} {mdnsHref ? ( {mdnsLabel} @@ -344,19 +361,21 @@ export function NetworkPage() { onClick={() => void runScan()} > - 扫描 WiFi + {tr("扫描 WiFi", "Scan WiFi")}

-

{scanStatus || "由设备端 NetworkManager 执行扫描"}

+

+ {scanStatus || tr("由设备端 NetworkManager 执行扫描", "Scanning is performed by NetworkManager on the device")} +

- - - + + + @@ -383,22 +402,22 @@ export function NetworkPage() {
-

Known Networks

+

{tr("已保存网络", "Known Networks")}

SSID信号安全操作{tr("信号", "Signal")}{tr("安全", "Security")}{tr("操作", "Action")}
- + - - + + @@ -406,7 +425,7 @@ export function NetworkPage() { - +
连接名{tr("连接名", "Connection")} SSID自动连接操作{tr("自动连接", "Auto-connect")}{tr("操作", "Action")}
{p.connection_name} {p.ssid}{p.autoconnect ? "是" : "否"}{p.autoconnect ? tr("是", "Yes") : tr("否", "No")}

{manualHint}

-

WiFi 引导

+

{tr("WiFi 引导", "WiFi Setup Guide")}

    -
  1. 连接热点 {wizardSsid},密码 ogscopeadmin
  2. -
  3. 浏览器打开 http://192.168.4.1:{httpPort()}
  4. -
  5. 扫描 WiFi 或手动填写 SSID 连接
  6. +
  7. + {tr("连接热点", "Join hotspot")} {wizardSsid}{tr(",密码", ", password")} {" "} + {showWizardPassword ? "ogscopeadmin" : "••••••••"}{" "} + +
  8. +
  9. {tr("浏览器打开", "Open in a browser")} http://192.168.4.1:{httpPort()}
  10. +
  11. {tr("扫描 WiFi 或手动填写 SSID 连接", "Scan WiFi or enter an SSID manually")}

Find Device

-

扫描常见网段并探测 /health

+

+ {tr("扫描常见网段并探测 /health", "Scan common subnets and probe /health")} +

{lanStatus}

diff --git a/web/spa/src/apps/system/pages/SensorsPage.tsx b/web/spa/src/apps/system/pages/SensorsPage.tsx index 04e65f7..1c9c193 100644 --- a/web/spa/src/apps/system/pages/SensorsPage.tsx +++ b/web/spa/src/apps/system/pages/SensorsPage.tsx @@ -26,7 +26,7 @@ function formatJson(data: unknown): string { } export function SensorsPage() { - const { t } = useI18n(); + const { t, locale } = useI18n(); const [bus, setBus] = useState(1); const [addr, setAddr] = useState(12); const [runI2cdetect, setRunI2cdetect] = useState(true); @@ -91,7 +91,11 @@ export function SensorsPage() { { method: "POST" }, ); setResult(data); - setMagCalHint("已开始方向校准,请缓慢旋转设备 5-15 秒。"); + setMagCalHint( + locale === "zh" + ? "已开始方向校准,请缓慢旋转设备 5-15 秒。" + : "Heading calibration started. Rotate the device slowly for 5–15 seconds.", + ); const status = await requestDevDebugJson( `/api/debug/sensors/magnetometer/calibration/status?${qs.toString()}`, ); @@ -102,7 +106,7 @@ export function SensorsPage() { } finally { setLoading(false); } - }, [addr, bus]); + }, [addr, bus, locale]); const runMagCalCommit = useCallback(async () => { setLoading(true); @@ -114,7 +118,7 @@ export function SensorsPage() { { method: "POST" }, ); setResult(data); - setMagCalHint("已保存并锁定方向校准。"); + setMagCalHint(locale === "zh" ? "已保存并锁定方向校准。" : "Heading calibration saved and locked."); const status = await requestDevDebugJson( `/api/debug/sensors/magnetometer/calibration/status?${qs.toString()}`, ); @@ -125,9 +129,14 @@ export function SensorsPage() { } finally { setLoading(false); } - }, [addr, bus]); + }, [addr, bus, locale]); const runMagCalReset = useCallback(async () => { + if (!window.confirm( + locale === "zh" + ? "重置已保存的方向校准并恢复自动模式?" + : "Reset the saved heading calibration and return to auto mode?", + )) return; setLoading(true); setError(null); try { @@ -137,7 +146,7 @@ export function SensorsPage() { { method: "POST" }, ); setResult(data); - setMagCalHint("已重置到自动模式。"); + setMagCalHint(locale === "zh" ? "已重置到自动模式。" : "Calibration reset to auto mode."); const status = await requestDevDebugJson( `/api/debug/sensors/magnetometer/calibration/status?${qs.toString()}`, ); @@ -148,7 +157,7 @@ export function SensorsPage() { } finally { setLoading(false); } - }, [addr, bus]); + }, [addr, bus, locale]); const runMagCalStatus = useCallback(async () => { setLoading(true); diff --git a/web/static/analysis-lab/assets/analysis-D_aCLSNL.js b/web/static/analysis-lab/assets/analysis-D_aCLSNL.js deleted file mode 100644 index 7584981..0000000 --- a/web/static/analysis-lab/assets/analysis-D_aCLSNL.js +++ /dev/null @@ -1,51 +0,0 @@ -var Os=Object.defineProperty;var $a=e=>{throw TypeError(e)};var Ps=(e,s,n)=>s in e?Os(e,s,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[s]=n;var F=(e,s,n)=>Ps(e,typeof s!="symbol"?s+"":s,n),Ta=(e,s,n)=>s.has(e)||$a("Cannot "+n);var f=(e,s,n)=>(Ta(e,s,"read from private field"),n?n.call(e):s.get(e)),ae=(e,s,n)=>s.has(e)?$a("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(e):s.set(e,n),De=(e,s,n,r)=>(Ta(e,s,"write to private field"),r?r.call(e,n):s.set(e,n),n);import{j as t,r as l,a as $s,R as Ts}from"./client-D1ZVDB-N.js";import{c as J,u as $e,T as es,I as As}from"./index-C78KOEFu.js";import{R as Us}from"./refresh-cw-gseCLMRP.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 Fe=J("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** - * @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 Bs=J("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** - * @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 Aa=J("Grid3x3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M3 9h18",key:"1pudct"}],["path",{d:"M3 15h18",key:"5xshup"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** - * @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 Hs=J("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/** - * @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 zs=J("House",[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",key:"5wwlr5"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"1d0kgt"}]]);/** - * @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 at=J("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** - * @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 Ua=J("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);/** - * @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 Ws=J("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** - * @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 Ba=J("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]);/** - * @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 Ha=J("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),_="/api/dev";async function V(e){if((e.headers.get("content-type")||"").includes("application/json"))return e.json();const n=await e.text();throw new Error(n||`HTTP ${e.status}`)}async function Vs(){const e=await fetch(`${_}/analysis/uploads`);if(!e.ok)throw new Error(await e.text());return e.json()}async function Gs(e,s){const n=new URLSearchParams;s!=null&&s.deleteExperiments&&n.set("delete_experiments","true");const r=n.toString(),o=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}${r?`?${r}`:""}`,{method:"DELETE"});if(!o.ok)throw new Error(await o.text());return await o.json().catch(()=>({}))}async function Ut(e,s="analysis_upload"){const n=new FormData;n.append("file",e),n.append("source",s);const r=await fetch(`${_}/analysis/upload`,{method:"POST",body:n}),o=await V(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}async function Ks(e){const s=await fetch(`${_}/analysis/uploads/import_from_debug`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:e})}),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function za(e){const s=await fetch(`${_}/analysis/uploads/replace_video`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function qs(e,s){const n=await fetch(`${_}/analysis/solve/image`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,...s})}),r=await V(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Js(e,s){const n=await fetch(`${_}/analysis/solve/batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,runs:s})}),r=await V(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Bt(e){const s=await fetch(`${_}/analysis/presets?scope=${e}`);if(!s.ok)throw new Error(await s.text());return s.json()}async function Zs(e,s){const n=await fetch(`${_}/analysis/presets`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,params:s})}),r=await V(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Ht(e){const s=await fetch(`${_}/analysis/experiments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Ys(e){const s=await fetch(`${_}/analysis/experiments/${encodeURIComponent(e)}`,{method:"DELETE"});if(!s.ok)throw new Error(await s.text())}async function zt(e,s,n=30){const r=new URLSearchParams({page:String(s),page_size:String(n)});e&&r.set("q",e);const o=await fetch(`${_}/analysis/experiments?${r}`);if(!o.ok)throw new Error(await o.text());return o.json()}async function Xs(){const e=await fetch(`${_}/debug/files`);if(!e.ok)throw new Error(await e.text());return e.json()}function Qs(e){return`${_}/debug/files/${encodeURIComponent(e)}`}async function Wa(e){const s=await fetch(`${_}/debug/files/${encodeURIComponent(e)}/info`),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function en(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/info`),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}function Wt(e){return`${_}/analysis/uploads/file?filename=${encodeURIComponent(e)}`}async function Va(e){const s=await fetch(`${_}/analysis/experiments/export?format=${e}`);if(!s.ok)throw new Error(await s.text());return s.text()}async function tn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/experiment_count`);if(!s.ok)throw new Error(await s.text());return s.json()}async function an(){const e=await fetch(`${_}/analysis/settings`);if(!e.ok)throw new Error(await e.text());return e.json()}async function sn(e){const s=await fetch(`${_}/analysis/solve/frame`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await V(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function nn(e,s){const n=new FormData;n.append("file",e,"frame.jpg"),n.append("payload",JSON.stringify(s));const r=await fetch(`${_}/analysis/solve/frame_upload`,{method:"POST",body:n}),o=await V(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}function rn(e){return`${_}/analysis/experiments/${encodeURIComponent(e)}/asset`}async function ln(){const e=await fetch("/api/dev/system/info");if(!e.ok)throw new Error(await e.text());return e.json()}function ts(e,s,n,r,o){var g,x,h,N;if(!r)return;if(e.clearRect(0,0,s,n),o.all&&Array.isArray(r.stars_all_centroids)){e.fillStyle="rgba(156, 163, 175, 0.85)";for(const m of r.stars_all_centroids)e.beginPath(),e.arc(m.x,m.y,2.4,0,Math.PI*2),e.fill()}if(o.rejected&&Array.isArray(r.stars_rejected_centroids)){e.strokeStyle="rgba(248, 113, 113, 0.95)",e.lineWidth=1.6;for(const m of r.stars_rejected_centroids)e.beginPath(),e.moveTo(m.x-4,m.y-4),e.lineTo(m.x+4,m.y+4),e.moveTo(m.x+4,m.y-4),e.lineTo(m.x-4,m.y+4),e.stroke()}if(o.pattern&&Array.isArray(r.stars_pattern)){e.strokeStyle="rgba(251, 146, 60, 0.95)",e.lineWidth=2;for(const m of r.stars_pattern)e.beginPath(),e.arc(m.x,m.y,6,0,Math.PI*2),e.stroke()}if(o.matched&&Array.isArray(r.stars_matched)){e.strokeStyle="rgba(34, 197, 94, 0.95)",e.fillStyle="rgba(34, 197, 94, 0.95)",e.lineWidth=2,e.font="11px system-ui, sans-serif";for(const m of r.stars_matched)e.beginPath(),e.arc(m.x,m.y,7,0,Math.PI*2),e.stroke(),m.mag!=null&&e.fillText(`m${Number(m.mag).toFixed(1)}`,m.x+4,m.y-4)}const u=r.overlay_ext;if(Array.isArray(u==null?void 0:u.labels_topn)&&u.labels_topn.length>0){e.fillStyle="rgba(96, 165, 250, 0.95)",e.font="12px system-ui, sans-serif";for(const m of u.labels_topn){if(typeof(m==null?void 0:m.x)!="number"||typeof(m==null?void 0:m.y)!="number")continue;const C=typeof m.mag=="number"?` m${m.mag.toFixed(1)}`:"",v=`${m.name??"Star"}${C}`;e.fillText(v,m.x+8,m.y+14)}}const c=u==null?void 0:u.polar_guide;if(c&&typeof((g=c.frame_center)==null?void 0:g.x)=="number"&&typeof((x=c.frame_center)==null?void 0:x.y)=="number"&&typeof((h=c.target)==null?void 0:h.x)=="number"&&typeof((N=c.target)==null?void 0:N.y)=="number"){const m=c.frame_center.x,C=c.frame_center.y,v=c.target.x,I=c.target.y;e.strokeStyle="rgba(244, 63, 94, 0.95)",e.fillStyle="rgba(244, 63, 94, 0.95)",e.lineWidth=2,e.beginPath(),e.moveTo(m,C),e.lineTo(v,I),e.stroke(),e.beginPath(),e.arc(v,I,6,0,Math.PI*2),e.stroke();const R=typeof c.angular_sep_deg=="number"?c.angular_sep_deg.toFixed(2):"-";e.font="12px system-ui, sans-serif",e.fillText(`Polar ${R}deg`,v+10,I-6)}}function Ga(e,s,n,r){if(!n)return;const o=s.naturalWidth||1,u=s.naturalHeight||1;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const c=e.getContext("2d");c&&ts(c,o,u,n,r)}function on(e,s,n,r){if(!n)return;const o=s.videoWidth||1,u=s.videoHeight||1;if(o<2||u<2)return;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const c=e.getContext("2d");c&&ts(c,o,u,n,r)}var j;(function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"})(j||(j={}));const cn=(()=>{let e=0;return()=>e++})(),dn=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),un=new Error("called FFmpeg.terminate()");var A,se,X,de,ue,lt,P;class mn{constructor(){ae(this,A,null);ae(this,se,{});ae(this,X,{});ae(this,de,[]);ae(this,ue,[]);F(this,"loaded",!1);ae(this,lt,()=>{f(this,A)&&(f(this,A).onmessage=({data:{id:s,type:n,data:r}})=>{switch(n){case j.LOAD:this.loaded=!0,f(this,se)[s](r);break;case j.MOUNT:case j.UNMOUNT:case j.EXEC:case j.FFPROBE:case j.WRITE_FILE:case j.READ_FILE:case j.DELETE_FILE:case j.RENAME:case j.CREATE_DIR:case j.LIST_DIR:case j.DELETE_DIR:f(this,se)[s](r);break;case j.LOG:f(this,de).forEach(o=>o(r));break;case j.PROGRESS:f(this,ue).forEach(o=>o(r));break;case j.ERROR:f(this,X)[s](r);break}delete f(this,se)[s],delete f(this,X)[s]})});ae(this,P,({type:s,data:n},r=[],o)=>f(this,A)?new Promise((u,c)=>{const g=cn();f(this,A)&&f(this,A).postMessage({id:g,type:s,data:n},r),f(this,se)[g]=u,f(this,X)[g]=c,o==null||o.addEventListener("abort",()=>{c(new DOMException(`Message # ${g} was aborted`,"AbortError"))},{once:!0})}):Promise.reject(dn));F(this,"load",({classWorkerURL:s,...n}={},{signal:r}={})=>(f(this,A)||(De(this,A,s?new Worker(new URL(s,import.meta.url),{type:"module"}):new Worker(new URL("/static/analysis-lab/assets/worker-BAOIWoxA.js",import.meta.url),{type:"module"})),f(this,lt).call(this)),f(this,P).call(this,{type:j.LOAD,data:n},void 0,r)));F(this,"exec",(s,n=-1,{signal:r}={})=>f(this,P).call(this,{type:j.EXEC,data:{args:s,timeout:n}},void 0,r));F(this,"ffprobe",(s,n=-1,{signal:r}={})=>f(this,P).call(this,{type:j.FFPROBE,data:{args:s,timeout:n}},void 0,r));F(this,"terminate",()=>{const s=Object.keys(f(this,X));for(const n of s)f(this,X)[n](un),delete f(this,X)[n],delete f(this,se)[n];f(this,A)&&(f(this,A).terminate(),De(this,A,null),this.loaded=!1)});F(this,"writeFile",(s,n,{signal:r}={})=>{const o=[];return n instanceof Uint8Array&&o.push(n.buffer),f(this,P).call(this,{type:j.WRITE_FILE,data:{path:s,data:n}},o,r)});F(this,"mount",(s,n,r)=>{const o=[];return f(this,P).call(this,{type:j.MOUNT,data:{fsType:s,options:n,mountPoint:r}},o)});F(this,"unmount",s=>{const n=[];return f(this,P).call(this,{type:j.UNMOUNT,data:{mountPoint:s}},n)});F(this,"readFile",(s,n="binary",{signal:r}={})=>f(this,P).call(this,{type:j.READ_FILE,data:{path:s,encoding:n}},void 0,r));F(this,"deleteFile",(s,{signal:n}={})=>f(this,P).call(this,{type:j.DELETE_FILE,data:{path:s}},void 0,n));F(this,"rename",(s,n,{signal:r}={})=>f(this,P).call(this,{type:j.RENAME,data:{oldPath:s,newPath:n}},void 0,r));F(this,"createDir",(s,{signal:n}={})=>f(this,P).call(this,{type:j.CREATE_DIR,data:{path:s}},void 0,n));F(this,"listDir",(s,{signal:n}={})=>f(this,P).call(this,{type:j.LIST_DIR,data:{path:s}},void 0,n));F(this,"deleteDir",(s,{signal:n}={})=>f(this,P).call(this,{type:j.DELETE_DIR,data:{path:s}},void 0,n))}on(s,n){s==="log"?f(this,de).push(n):s==="progress"&&f(this,ue).push(n)}off(s,n){s==="log"?De(this,de,f(this,de).filter(r=>r!==n)):s==="progress"&&De(this,ue,f(this,ue).filter(r=>r!==n))}}A=new WeakMap,se=new WeakMap,X=new WeakMap,de=new WeakMap,ue=new WeakMap,lt=new WeakMap,P=new WeakMap;var Ka;(function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"})(Ka||(Ka={}));const xn=new Error("failed to get response body reader"),pn=new Error("failed to complete download"),fn="Content-Length",hn=e=>new Promise((s,n)=>{const r=new FileReader;r.onload=()=>{const{result:o}=r;o instanceof ArrayBuffer?s(new Uint8Array(o)):s(new Uint8Array)},r.onerror=o=>{var u,c;n(Error(`File could not be read! Code=${((c=(u=o==null?void 0:o.target)==null?void 0:u.error)==null?void 0:c.code)||-1}`))},r.readAsArrayBuffer(e)}),bn=async e=>{let s;if(typeof e=="string")/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?s=atob(e.split(",")[1]).split("").map(n=>n.charCodeAt(0)):s=await(await fetch(e)).arrayBuffer();else if(e instanceof URL)s=await(await fetch(e)).arrayBuffer();else if(e instanceof File||e instanceof Blob)s=await hn(e);else return new Uint8Array;return new Uint8Array(s)},vn=async(e,s)=>{var o;const n=await fetch(e);let r;try{const u=parseInt(n.headers.get(fn)||"-1"),c=(o=n.body)==null?void 0:o.getReader();if(!c)throw xn;const g=[];let x=0;for(;;){const{done:m,value:C}=await c.read(),v=C?C.length:0;if(m){if(u!=-1&&u!==x)throw pn;s&&s({url:e,total:u,received:x,delta:v,done:m});break}g.push(C),x+=v,s&&s({url:e,total:u,received:x,delta:v,done:m})}const h=new Uint8Array(x);let N=0;for(const m of g)h.set(m,N),N+=m.length;r=h.buffer}catch(u){console.log("failed to send download progress event: ",u),r=await n.arrayBuffer()}return r},qa=async(e,s,n=!1,r)=>{const o=n?await vn(e,r):await(await fetch(e)).arrayBuffer(),u=new Blob([o],{type:s});return URL.createObjectURL(u)};let Vt=null,Oe=null,O;async function gn(){if(Vt)return Vt;if(Oe)return Oe;Oe=(async()=>{const e=new mn;e.on("progress",({progress:o})=>{O==null||O(o,"transcoding")}),O==null||O(.01,"loading_ffmpeg");const s="https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm",n=await qa(`${s}/ffmpeg-core.js`,"text/javascript");O==null||O(.04,"loading_ffmpeg");const r=await qa(`${s}/ffmpeg-core.wasm`,"application/wasm");return O==null||O(.07,"loading_ffmpeg"),await e.load({coreURL:n,wasmURL:r}),Vt=e,e})();try{return await Oe}finally{Oe=null}}async function yn(e){const s=URL.createObjectURL(e);try{return await new Promise(r=>{const o=document.createElement("video");o.preload="metadata",o.onloadedmetadata=()=>{const u=Number(o.duration);r(Number.isFinite(u)&&u>0?u:null)},o.onerror=()=>r(null),o.src=s})}finally{URL.revokeObjectURL(s)}}async function Ja(e,s){const n=O;O=s;try{const r=await gn(),o="input.avi",u="output.mp4";s==null||s(.1,"writing_input"),await r.writeFile(o,await bn(e)),await r.exec(["-i",o,"-c:v","libx264","-preset","veryfast","-crf","24","-pix_fmt","yuv420p","-movflags","+faststart","-an",u]);const c=await r.readFile(u);s==null||s(.98,"packing_output");const g=new Blob([c],{type:"video/mp4"}),x=new File([g],e.name.replace(/\.avi$/i,".mp4"),{type:"video/mp4"});await r.deleteFile(o),await r.deleteFile(u);const h=await yn(x);return s==null||s(1,"done"),{file:x,duration_s:h}}finally{O=n}}function Jt(e){return e==null||Number.isNaN(e)?"—":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(2)} MB`}function Pe(e,s){if(!e)return"—";try{const n=new Date(e);return new Intl.DateTimeFormat(s==="en"?"en-GB":"zh-CN",{dateStyle:"short",timeStyle:"medium"}).format(n)}catch{return e}}function ce(e){return e==null?null:typeof e=="string"&&e.trim()?e.trim():typeof e=="number"&&!Number.isNaN(e)?String(e):null}function wn(e){return typeof e!="number"||e<=0?null:e>=1e6?`${(e/1e6).toFixed(2)} s`:e>=1e3?`${(e/1e3).toFixed(0)} ms`:`${e} µs`}function jn(e,s){if(!e)return[];const n=[],r=wn(e.exposure_us);r&&n.push({key:"meta.exposure",value:r});const o=ce(e.analogue_gain),u=ce(e.digital_gain);if(o||u){const C=[o?`A ${o}`:"",u?`D ${u}`:""].filter(Boolean);n.push({key:"meta.gain",value:C.join(" · ")})}const c=ce(e.fps);c&&n.push({key:"meta.fps",value:c});const g=ce(e.sensor);g&&n.push({key:"meta.sensor",value:g});const x=ce(e.color_mode);x&&n.push({key:"meta.colorMode",value:x});const h=ce(e.resolution);h&&n.push({key:"meta.outputResolution",value:h});const N=ce(e.modified);N&&n.push({key:"meta.fileTime",value:Pe(N,s)});const m=e.size;return typeof m=="number"&&m>0&&n.push({key:"meta.fileSize",value:Jt(m)}),n}function as(e){const s=n=>typeof n=="number"&&!Number.isNaN(n)?n:null;return e?{tSolveMs:s(e.t_solve_ms),tExtractMs:s(e.t_extract_ms),tPreprocessMs:s(e.t_preprocess_ms),tOpenDecodeMs:s(e.t_open_decode_ms),tBackendTotalMs:s(e.t_backend_total_ms),raDeg:s(e.ra_deg),decDeg:s(e.dec_deg),matches:s(e.matches),rmseArcsec:s(e.rmse_arcsec),prob:s(e.prob),status:typeof e.status=="string"?e.status:null}:{tSolveMs:null,tExtractMs:null,tPreprocessMs:null,tOpenDecodeMs:null,tBackendTotalMs:null,raDeg:null,decDeg:null,matches:null,rmseArcsec:null,prob:null,status:null}}function it(e){return e==null?"—":`${e.toFixed(4)}°`}function ss(e){if(e==null||typeof e!="number"||Number.isNaN(e)||e<0)return null;if(e>=1)return 0;if(e===0)return 100;const s=(1-e)*100;if(s<=100-1e-12)return Math.min(100,Math.max(0,s));const n=-Math.log10(Math.max(e,Number.MIN_VALUE)),r=14,u=Math.min(1,Math.max(0,(n-r)/(50-r)));return Math.min(100,Math.max(0,70+30*u))}function Nn(e){const s=ss(e);return s==null?"—":`${s.toFixed(1)}%`}function _n(e){if(e==null)return"—";if(typeof e=="number"&&!Number.isNaN(e)){const s=Math.abs(e);return s>0&&s<.001?e.toExponential(4):s>=0&&s<=1?`${(e*100).toFixed(4)}%`:String(e)}return String(e)}function ge(e,s){const n=s==null?void 0:s.tetra,r=n?n.Prob??n.prob:void 0,o=Nn(e),u=r!=null?_n(r):null;return{line:o,rawLine:u}}function st({result:e,t:s,roundTripMs:n}){const r=as(e??void 0);return e?t.jsxs("div",{className:"mt-2 space-y-2 text-[10px]",children:[t.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[r.tBackendTotalMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.backendTotalMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tBackendTotalMs.toFixed(0)," ms"]})]}),r.tSolveMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveComputeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tSolveMs.toFixed(0)," ms"]})]}),r.tOpenDecodeMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.openDecodeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tOpenDecodeMs.toFixed(0)," ms"]})]}),r.tPreprocessMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.preprocessMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tPreprocessMs.toFixed(0)," ms"]})]}),r.tExtractMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.extractMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tExtractMs.toFixed(0)," ms"]})]}),n!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveRoundTripMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[n.toFixed(0)," ms"]})]}),r.matches!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.matches")}),t.jsx("div",{className:"font-semibold tabular-nums text-on-surface",children:r.matches})]}),r.rmseArcsec!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.rmse")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.rmseArcsec.toFixed(2),"″"]})]}),r.prob!=null&&t.jsxs("div",{className:"col-span-2",children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.prob")}),t.jsx("div",{className:"font-semibold text-on-surface",children:ge(r.prob,e).line}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/90",children:s("lab.metric.probHelp")}),ge(r.prob,e).rawLine&&t.jsxs("div",{className:"mt-1 text-[9px] text-on-surface-variant",children:[s("lab.metric.probRaw"),": ",ge(r.prob,e).rawLine,t.jsx("p",{className:"mt-0.5 text-[8px] leading-snug opacity-90",children:s("lab.metric.probRawHelp")})]})]})]}),e.centroid_quality!=null&&typeof e.centroid_quality=="object"&&t.jsxs("div",{className:"rounded border border-outline-variant/25 bg-surface-container-high/40 p-2",children:[t.jsxs("div",{className:"text-[9px] font-medium text-on-surface-variant",children:[s("lab.centroidQualityTitle"),typeof e.centroid_quality.level=="number"?` · L${e.centroid_quality.level}`:""]}),Array.isArray(e.centroid_quality.hints)&&e.centroid_quality.hints.length>0?t.jsx("ul",{className:"mt-1 list-inside list-disc text-[9px] leading-snug text-on-surface-variant",children:e.centroid_quality.hints.map((o,u)=>t.jsx("li",{children:o},u))}):null,(()=>{const o=e.centroid_quality.metrics;if(!o)return null;const u=o.input_count,c=o.output_count,g=o.removed_dense,x=o.removed_line;return typeof u!="number"||typeof c!="number"||typeof g!="number"||typeof x!="number"?null:t.jsx("p",{className:"mt-1 font-mono text-[9px] text-on-surface-variant/95",children:s("lab.centroidQualityMetrics",{in:u,out:c,dense:g,line:x})})})()]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.radec")}),t.jsxs("div",{className:"font-mono text-[9px] text-on-surface",children:["α ",it(r.raDeg)," · δ ",it(r.decDeg)]})]}),r.status&&t.jsxs("div",{className:"rounded bg-surface-container-high px-2 py-1 text-[9px] font-mono text-on-surface",children:[s("lab.metric.status"),": ",r.status]})]}):t.jsx("p",{className:"mt-2 text-[10px] text-on-surface-variant",children:"—"})}function W({label:e,helpKey:s,value:n,onChange:r,type:o="number",step:u}){const{t:c}=$e(),g=s?c(s):void 0;return t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e}),g&&t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:g}),t.jsx("input",{type:o,step:u,className:"w-full rounded bg-surface-container-highest px-2 py-1",value:n===""?"":n,onChange:x=>{const h=x.target.value;r(h===""?void 0:Number(h))}})]})}function Sn({view:e,setView:s}){const{t:n,locale:r,setLocale:o}=$e();return t.jsxs("header",{className:"flex h-12 shrink-0 items-center justify-between border-b border-outline-variant/20 px-4",children:[t.jsxs("div",{className:"flex items-center gap-6",children:[t.jsx("span",{className:"font-headline text-sm font-bold tracking-wide text-on-surface",children:n("app.title")}),t.jsxs("nav",{className:"flex flex-wrap gap-3 text-xs",children:[t.jsx("button",{type:"button",className:e==="lab_image"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_image"),children:n("nav.labImage")}),t.jsx("button",{type:"button",className:e==="lab_video"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_video"),children:n("nav.labVideo")}),t.jsx("button",{type:"button",className:e==="pool"?"text-primary":"text-on-surface-variant",onClick:()=>s("pool"),children:n("nav.pool")}),t.jsx("button",{type:"button",className:e==="history"?"text-primary":"text-on-surface-variant",onClick:()=>s("history"),children:n("nav.history")})]})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("zh"),children:n("lang.zh")}),t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("en"),children:n("lang.en")})]}),t.jsxs("a",{className:"flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 text-xs text-on-surface-variant hover:bg-surface-container",href:"/debug",children:[t.jsx(zs,{className:"h-3.5 w-3.5"})," ",n("nav.systemAdmin")]})]})]})}function kn({historyQ:e,setHistoryQ:s,historyPage:n,setHistoryPage:r,historyData:o,historyExpandId:u,setHistoryExpandId:c,historyTotalPages:g,onDeleteExperiment:x,onSearch:h}){var C;const{t:N,locale:m}=$e();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsx("p",{className:"mb-4 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 p-3 text-xs leading-relaxed text-on-surface-variant",children:N("history.intro")}),t.jsxs("div",{className:"mb-4 flex flex-wrap items-center gap-4",children:[t.jsxs("h2",{className:"flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Hs,{className:"h-5 w-5"})," ",N("history.title")]}),t.jsx("input",{className:"rounded bg-surface-container-highest px-2 py-1 text-xs",placeholder:N("history.search"),value:e,onChange:v=>s(v.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-1 text-xs",onClick:h,children:N("history.searchBtn")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>Va("json").then(v=>{const I=new Blob([v],{type:"application/json"}),R=document.createElement("a");R.href=URL.createObjectURL(I),R.download="experiments.json",R.click()}),children:N("history.exportJson")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>Va("csv").then(v=>{const I=new Blob([v],{type:"text/csv"}),R=document.createElement("a");R.href=URL.createObjectURL(I),R.download="experiments.csv",R.click()}),children:N("history.exportCsv")})]}),t.jsxs("div",{className:"flex flex-wrap items-center gap-3 text-xs text-on-surface-variant",children:[t.jsx("span",{children:N("history.total",{n:(o==null?void 0:o.total)??0})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n<=1,onClick:()=>r(v=>Math.max(1,v-1)),children:N("history.prev")}),t.jsxs("span",{children:[n," / ",g]}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n>=g,onClick:()=>r(v=>v+1),children:N("history.next")})]}),t.jsx("ul",{className:"mt-2 space-y-2",children:(C=o==null?void 0:o.items)==null?void 0:C.map(v=>{const I=String(v.id??""),R=v.metrics,B=u===I;return t.jsxs("li",{className:"rounded border border-outline-variant/20 p-2 text-[11px]",children:[t.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-2 font-mono",children:[t.jsxs("div",{children:[t.jsxs("div",{className:"text-on-surface",children:[Pe(String(v.created_at??""),m)," —"," ",String(v.input_name)," — ",String(v.preset_label)]}),t.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[N("history.preset"),": ",String(v.preset_label)," · ",N("history.metrics"),":"," ","matches=",String((R==null?void 0:R.matches)??"—")," rmse=",String((R==null?void 0:R.rmse_arcsec)??"—")]})]}),t.jsxs("div",{className:"flex shrink-0 gap-1",children:[t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 text-[10px]",onClick:()=>c(B?null:I),children:N(B?"history.collapse":"history.detail")}),t.jsx("button",{type:"button",className:"rounded px-2 py-0.5 text-[10px] text-error hover:bg-error-container/20",title:N("history.delete"),onClick:()=>void x(I),children:N("history.delete")})]})]}),B&&t.jsxs("div",{className:"mt-2 space-y-2",children:[v.asset_snapshot_relpath?t.jsx("img",{src:rn(I),alt:"",className:"max-h-48 max-w-full rounded border border-outline-variant/20 object-contain"}):null,t.jsx("pre",{className:"og-scrollbar max-h-64 overflow-auto rounded bg-surface-container p-2 text-[10px]",children:JSON.stringify(v.result_json,null,2)})]})]},I)})})]})}function En({uploads:e,onDelete:s}){const{t:n,locale:r}=$e();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsxs("h2",{className:"mb-4 flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Bs,{className:"h-5 w-5"})," ",n("pool.title")]}),t.jsxs("table",{className:"w-full text-left text-xs",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-outline-variant/30 text-on-surface-variant",children:[t.jsx("th",{className:"py-2",children:n("pool.col.name")}),t.jsx("th",{className:"py-2",children:n("pool.col.source")}),t.jsx("th",{className:"py-2",children:n("pool.col.size")}),t.jsx("th",{className:"py-2",children:n("pool.col.time")}),t.jsx("th",{className:"w-16 py-2 text-center",children:n("pool.delete")})]})}),t.jsx("tbody",{children:e.map(o=>t.jsxs("tr",{className:"border-b border-outline-variant/10",children:[t.jsx("td",{className:"py-2 font-mono",children:o.filename}),t.jsx("td",{className:"py-2",children:o.source??n("common.placeholder")}),t.jsx("td",{className:"py-2",children:Jt(o.size)}),t.jsx("td",{className:"py-2",children:Pe(o.modified_at,r)}),t.jsx("td",{className:"py-2 text-center",children:t.jsx("button",{type:"button",className:"inline-flex rounded p-1 text-on-surface-variant hover:bg-error-container/30 hover:text-error",title:n("pool.delete"),"aria-label":n("pool.delete"),onClick:()=>void s(o.filename),children:t.jsx(es,{className:"h-3.5 w-3.5"})})})]},o.filename))})]})]})}function Gt(e){return e?{matches:e.matches,rmse_arcsec:e.rmse_arcsec,status:e.status,prob:e.prob,t_solve_ms:e.t_solve_ms}:{}}function Rn(e){var n,r;if(!e)return null;const s=e.solve_overlay;return(n=s==null?void 0:s.stars_matched)!=null&&n.length?s.stars_matched.length:(r=s==null?void 0:s.stars_all_centroids)!=null&&r.length?s.stars_all_centroids.length:typeof e.matches=="number"?e.matches:null}function Za(e){return/\.(jpe?g|png|webp|bmp|gif|fits?)$/i.test(e)}function ve(e){return/\.(mp4|mov|webm|mkv|avi)$/i.test(e)}function nt(e,s=16,n=14){return e.length<=s+n+1?e:`${e.slice(0,s)}...${e.slice(-n)}`}const Ya=10,rt=30,Kt=6,qt=()=>({hint_ra_deg:45,hint_dec_deg:80,fov_estimate:11,fov_max_error:void 0,solve_timeout_ms:1500,solve_profile:"balanced",max_image_side:1280,large_scale_bg_subtract:!1,detail_level:"summary",centroid_rejection_level:3,centroid:{sigma:2.5,max_area:400,min_area:5,filtsize:25,binary_open:!0,max_axis_ratio:void 0}});function Xa(e,s,n,r){const o=typeof r=="number"&&r>0?` ${e("lab.gateNextMs",{ms:r})}`:"",u=String(n??"").toLowerCase();return s==="SKIPPED_BUSY"?u.includes("recording")?`${e("lab.gate.skipRecording")}${o}`:u.includes("previous")?`${e("lab.gate.skipBusyInFlight")}${o}`:`${e("lab.gate.skipBusyFallback",{detail:String(n??"")})}${o}`:s==="SKIPPED_INTERVAL"?`${e("lab.gate.skipInterval")}${o}`:`${s??"?"}${n?`: ${String(n)}`:""}${o}`}async function Qa(){try{const e=await fetch("/api/dev/debug/camera/stream/status",{cache:"no-store",credentials:"same-origin"});if(!e.ok)return"fail";const s=await e.json();if(s.success===!1)return"fail";const n=Number(s.max_clients??0),r=Number(s.active_clients??0);return n>0&&r>=n?"busy":"ok"}catch{return"fail"}}function Mn(){var Ma,Ca,Ia,La,Da,Fa,Oa;const{t:e,locale:s}=$e(),[n,r]=l.useState("lab_image"),[o,u]=l.useState([]),[c,g]=l.useState(null),[x,h]=l.useState(qt),[N,m]=l.useState([]),[C,v]=l.useState([]),[I,R]=l.useState(new Set),[B,U]=l.useState(!1),[ot,y]=l.useState(null),[S,ye]=l.useState(null),[T,we]=l.useState(null),[Te,Zt]=l.useState([]),[ns,Yt]=l.useState([]),Xt=l.useRef(null),Qt=l.useRef(null),[je,rs]=l.useState(""),[Ae,ea]=l.useState(1),[Ue,ct]=l.useState(null),[ta,aa]=l.useState(null),[dt,sa]=l.useState(""),[G,is]=l.useState({matched:!0,pattern:!0,all:!0,rejected:!0}),[Be,na]=l.useState([]),[H,ra]=l.useState(null),[Ne,_e]=l.useState(1),[He,ia]=l.useState(!1),[me,ut]=l.useState(1),[la,ls]=l.useState({w:0,h:0}),[xe,os]=l.useState({w:0,h:0}),[oa,ca]=l.useState({w:0,h:0}),[k,Se]=l.useState("file"),[da,ze]=l.useState(()=>Date.now()),[mt,K]=l.useState("unknown"),[ke,xt]=l.useState(null),[Ee,ua]=l.useState(!1),[pe,ma]=l.useState(!1),[xa,cs]=l.useState(!1),[q,pt]=l.useState(!1),[ds,ft]=l.useState(null),[ht,bt]=l.useState(null),[vt,We]=l.useState(null),[gt,pa]=l.useState(!1),[us,yt]=l.useState({}),[fa,wt]=l.useState(!1),[jt,Z]=l.useState(null),[ms,Re]=l.useState(null),[Nt,Me]=l.useState(null),[_t,ha]=l.useState(null),[ba,va]=l.useState(!1),[ga,St]=l.useState(null),[xs,Q]=l.useState(null),[ya,wa]=l.useState(!1),[kt,ne]=l.useState(null),[ps,z]=l.useState(null),[Et,Rt]=l.useState(null),Mt=l.useRef(null),Ce=l.useRef(null),Ie=l.useRef(null),Ct=l.useRef(0),re=l.useRef(null),ie=l.useRef(null),It=l.useRef(!1),Lt=l.useRef(!1),Dt=l.useRef(!1),Ft=l.useRef(!1),fe=l.useRef(null),fs=l.useCallback(async()=>{const a=Ie.current;if(!a||a.naturalWidth<2)return null;const i=document.createElement("canvas");i.width=a.naturalWidth,i.height=a.naturalHeight;const d=i.getContext("2d");if(!d)return null;try{d.drawImage(a,0,0)}catch{return null}return await new Promise(p=>{i.toBlob(w=>p(w?URL.createObjectURL(w):null),"image/jpeg",.92)})},[]),[Ve,hs]=l.useState(null),[L,ja]=l.useState(null),Ge=l.useCallback((a,i)=>{const d=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;Yt(p=>[...p,{id:d,text:a,variant:i}]),window.setTimeout(()=>{Yt(p=>p.filter(w=>w.id!==d))},4800)},[]);l.useEffect(()=>{Xt.current=S},[S]),l.useEffect(()=>{Qt.current=T},[T]),l.useEffect(()=>{Dt.current=pe},[pe]),l.useEffect(()=>{Ft.current=Ee},[Ee]),l.useEffect(()=>{ot&&(Ge(ot,"error"),y(null))},[ot,Ge]),l.useEffect(()=>{Nt&&(Ge(Nt,"warn"),Me(null))},[Nt,Ge]);const Ke=l.useCallback(()=>{const a=Xt.current,i=Qt.current;if(!a&&!i)return;const d=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;Zt(p=>[{id:d,at:Date.now(),single:a??void 0,batch:i??void 0},...p].slice(0,Ya))},[]),qe=()=>{ie.current!=null&&(window.clearTimeout(ie.current),ie.current=null)},Je=()=>{re.current!=null&&(window.clearTimeout(re.current),re.current=null)},Ot=(L==null?void 0:L.star_analysis_min_interval_ms)??2e3,Pt=(L==null?void 0:L.star_analysis_max_interval_ms)??12e3,$t=l.useMemo(()=>{const a=(L==null?void 0:L.star_analysis_target_fps)??.6666666666666666,i=Math.min(Math.max(a,.2),5);return Math.round(1e3/i)},[L]),le=l.useMemo(()=>Math.min(Math.max(_t??$t,Ot),Pt),[$t,_t,Pt,Ot]),ee=l.useCallback(async()=>{const[a,i,d]=await Promise.all([Vs(),Bt("official"),Bt("user")]);u(a.files),m(i.presets),v(d.presets)},[]),Tt=l.useCallback(()=>{Xs().then(a=>na(a.files)).catch(()=>na([]))},[]);l.useEffect(()=>{an().then(a=>{ja(a);const i=Math.round(1e3/Math.min(Math.max(a.star_analysis_target_fps,.2),5)),d=Math.min(Math.max(i,a.star_analysis_min_interval_ms),a.star_analysis_max_interval_ms);ha(d);const p=a.centroid_rejection_level_default;typeof p=="number"&&p>=1&&p<=5&&h(w=>({...w,centroid_rejection_level:p}))}).catch(()=>ja(null))},[]),l.useEffect(()=>{ee().catch(a=>y(String(a)))},[ee]),l.useEffect(()=>{Tt()},[Tt]),l.useEffect(()=>{n==="history"&&zt(je,Ae,rt).then(ct).catch(a=>y(String(a)))},[n,je,Ae]),l.useEffect(()=>{if(!c){We(null);return}let a=!1;return pa(!0),(async()=>{try{const i=await en(c);if(a)return;let d={...i};try{d={...await Wa(c),...i}}catch{}We(d)}catch{try{const i=await Wa(c);a||We(i)}catch{a||We(null)}}finally{a||pa(!1)}})(),()=>{a=!0}},[c]),l.useEffect(()=>{ye(null),we(null),yt({}),wt(!1),Z(null),Re(null),Se("file"),xt(null),Zt([])},[c]);const Y=l.useMemo(()=>{const a=S==null?void 0:S.result;if(!a)return null;const i=a.solve_overlay||null;if(!i)return null;const d=a.overlay_ext||null;return{...i,overlay_ext:d||void 0}},[S]),te=l.useMemo(()=>(S==null?void 0:S.result)??null,[S]),Na=l.useMemo(()=>Rn(te),[te]),At=l.useMemo(()=>n==="lab_image"?la:n==="lab_video"?k==="file"?xe:oa:{w:0,h:0},[n,k,la,xe,oa]),Ze=c?Wt(c):"";l.useEffect(()=>{if(n!=="lab_image")return;const a=Mt.current,i=fe.current;if(!a||!i||!Y||!c)return;const d=()=>Ga(i,a,Y,G);a.complete?d():a.onload=d},[Y,c,S,G,n]),l.useEffect(()=>{if(n!=="lab_video"||k!=="file")return;const a=Ce.current,i=fe.current;if(!a||!i||!Y||!c)return;const d=()=>on(i,a,Y,G);return a.addEventListener("loadeddata",d),a.addEventListener("seeked",d),a.readyState>=2&&d(),()=>{a.removeEventListener("loadeddata",d),a.removeEventListener("seeked",d)}},[Y,G,n,k,c,S]),l.useEffect(()=>{if(n!=="lab_video"||k!=="camera")return;const a=Ie.current,i=fe.current;if(!a||!i||!Y)return;const d=()=>Ga(i,a,Y,G);a.complete?d():a.onload=d},[Y,G,n,k,S,da,q]),l.useEffect(()=>{if(n!=="lab_video"||k!=="camera"){K("unknown");return}if(q){K("ok");return}let a=!1;return K("unknown"),(async()=>{try{const i=await Qa();if(a)return;if(i==="fail"){K("fail");return}if(i==="busy"){K("busy");return}K("ok"),Ct.current=0,ze(Date.now())}catch{a||K("fail")}})(),()=>{a=!0}},[n,k,q]),l.useEffect(()=>{const a=Mt.current;if(!a)return;const i=()=>ls({w:a.naturalWidth||0,h:a.naturalHeight||0});return i(),a.addEventListener("load",i),()=>a.removeEventListener("load",i)},[c,Ze]),l.useEffect(()=>{const a=Ce.current;if(!a)return;const i=()=>os({w:a.videoWidth||0,h:a.videoHeight||0});return a.addEventListener("loadedmetadata",i),a.addEventListener("loadeddata",i),i(),()=>{a.removeEventListener("loadedmetadata",i),a.removeEventListener("loadeddata",i)}},[c,Ze,n]);const bs=a=>{h({...qt(),...a,centroid:{...qt().centroid,...a.centroid}})},vs=async()=>{if(!c){y(e("err.selectFile"));return}y(null),U(!0);const a=performance.now();try{const i=await qs(c,x);Ke(),ye(i),we(null),Re("file"),Z(performance.now()-a)}catch(i){y(String(i)),Z(null)}finally{U(!1)}},gs=async()=>{if(!c){y(e("err.selectFile"));return}const a=[];for(const d of I){const w=[...N,...C].find($=>$.id===d);w&&a.push({label:w.name,params:structuredClone(w.params)})}if(a.length===0){y(e("err.selectPresets"));return}y(null),U(!0);const i=performance.now();try{const d=await Js(c,a);Ke(),we({results:d.results}),ye(null),yt({}),Re("file"),Z(performance.now()-i)}catch(d){y(String(d)),Z(null)}finally{U(!1)}},Ye=async a=>{if(It.current)return;y(null),It.current=!0;const i=performance.now();try{const d=await sn({source:"camera",overlay_topn_count:3,enable_polar_guide:!0,solve_interval_ms:le,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),...x}),p=d.gate_status,w=d.next_allowed_in_ms,$=d.effective_interval_ms;if(p==="SKIPPED_INTERVAL"||p==="SKIPPED_BUSY"){if(Me(Xa(e,p,d.gate_reason,typeof w=="number"?w:null)),a){Je();const M=Math.max(50,Number(w??le));re.current=window.setTimeout(()=>{re.current=null,Ft.current&&Ye(!0)},M)}return}Me(null),d.result!=null&&(Ke(),ye(d)),we(null),Re("camera"),Se("camera");const E=d.result,D=typeof(E==null?void 0:E.status)=="string"?String(E.status):"";if(xa&&D==="MATCH_FOUND"){pt(!0),ft(d.frame_id!=null?String(d.frame_id):null);const M=await fs();bt(oe=>(oe&&URL.revokeObjectURL(oe),M)),et()}else if(a){Je();const M=Math.max(50,Number(w??$??le));re.current=window.setTimeout(()=>{re.current=null,Ft.current&&Ye(!0)},M)}Z(performance.now()-i)}catch(d){y(String(d)),Z(null)}finally{It.current=!1}},ys=()=>void Ye(!1),Xe=async a=>{if(Lt.current||!c)return;const i=Ce.current;if(!i||i.videoWidth<2||i.videoHeight<2||ke)return;y(null),Lt.current=!0;const d=performance.now();try{const p=document.createElement("canvas");p.width=i.videoWidth,p.height=i.videoHeight;const w=p.getContext("2d");if(!w)throw new Error("无法创建画布上下文 / Cannot create canvas context");w.drawImage(i,0,0,p.width,p.height);const $=await new Promise((be,Fs)=>{p.toBlob(Pa=>Pa?be(Pa):Fs(new Error("帧编码失败 / Frame encode failed")),"image/jpeg",.92)}),E=await nn($,{...x,solve_interval_ms:le,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),overlay_topn_count:3,enable_polar_guide:!0}),D=E.gate_status,M=E.next_allowed_in_ms,oe=E.effective_interval_ms;if(D==="SKIPPED_INTERVAL"||D==="SKIPPED_BUSY"){if(Me(Xa(e,D,E.gate_reason,typeof M=="number"?M:null)),a){qe();const be=Math.max(50,Number(M??le));ie.current=window.setTimeout(()=>{ie.current=null,Dt.current&&Xe(!0)},be)}return}if(Me(null),E.result!=null&&(Ke(),ye(E)),we(null),Re("file"),Z(performance.now()-d),a){qe();const be=Math.max(50,Number(M??oe??le));ie.current=window.setTimeout(()=>{ie.current=null,Dt.current&&Xe(!0)},be)}}catch(p){y(String(p)),Z(null)}finally{Lt.current=!1}},ws=()=>void Xe(!1),js=()=>{pe||!c||(ma(!0),qe(),Xe(!0))},_a=l.useMemo(()=>n!=="lab_video"||k!=="file"||!c||/\.avi$/i.test(c)||ke?!1:xe.w>1&&xe.h>1,[n,k,c,ke,xe.w,xe.h]),Sa=l.useMemo(()=>!!c&&/\.avi$/i.test(c),[c]),Ns=async()=>{if(!(!c||!Sa||ba)){y(null),va(!0),St(0),Q(e("lab.transcode.loading"));try{const a=Wt(c),i=await fetch(a,{cache:"no-store"});if(!i.ok)throw new Error(e("lab.transcode.fetchFailed"));const d=await i.blob(),p=new File([d],c,{type:"video/x-msvideo"}),w=await Ja(p,(E,D)=>{St(Math.max(0,Math.min(1,E))),D==="loading_ffmpeg"?Q(e("lab.transcode.loading")):D==="writing_input"?Q(e("lab.transcode.writingBuffer")):D==="transcoding"?Q(e("lab.transcode.running")):D==="packing_output"&&Q(e("lab.transcode.packaging"))});Q(e("lab.transcode.uploading"));const $=await Ut(w.file,"analysis_transcoded");await za({old_filename:c,new_filename:$.filename,duration_s:w.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),await ee(),g($.filename),St(1),Q(e("lab.transcode.done"))}catch(a){y(String(a)),Q(e("lab.transcode.failed"))}finally{va(!1)}}},_s=async a=>{z(e("sidebar.flowDownloadingPool")),ne(.19);const i=Wt(a),d=await fetch(i,{cache:"no-store"});if(!d.ok)throw new Error(e("lab.transcode.fetchFailed"));const p=await d.blob(),w=new File([p],a,{type:"video/x-msvideo"}),$=await Ja(w,(D,M)=>{ne(.2+Math.max(0,Math.min(1,D))*.6),M==="loading_ffmpeg"?z(e("sidebar.flowLoadingTranscoder")):M==="writing_input"?z(e("sidebar.flowWritingBuffer")):M==="transcoding"?z(e("sidebar.flowTranscoding")):M==="packing_output"&&z(e("sidebar.flowPackaging"))});z(e("sidebar.flowUploadingMp4")),ne(.86);const E=await Ut($.file,"debug_console_transcoded");return z(e("sidebar.flowReplacing")),ne(.94),await za({old_filename:a,new_filename:E.filename,duration_s:$.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),E.filename},Ss=async()=>{if(!H||ya)return;const a=/\.avi$/i.test(H);y(null),wa(!0),ne(.02),Rt(null);try{z(e("sidebar.flowImportingDebug"));const i=await Ks(H);ne(a?.18:1);let d=i.filename;a?d=await _s(i.filename):z(e("sidebar.flowNoTranscode")),await ee(),g(d),r(ve(d)?"lab_video":"lab_image"),ne(1),z(e("sidebar.flowDone")),Rt(e("sidebar.flowDoneMsg",{name:nt(d)}))}catch(i){y(String(i)),z(e("sidebar.flowFailed")),Rt(String(i))}finally{wa(!1)}},Qe=()=>{ma(!1),qe()},ks=()=>{Ee||q||(ua(!0),Je(),Ye(!0))},et=()=>{ua(!1),Je()},Es=async()=>{try{const a=Ie.current;a&&(a.onload=null,a.onerror=null,a.src="",a.removeAttribute("src")),await new Promise(i=>window.requestAnimationFrame(()=>i())),await fetch("/api/dev/debug/camera/stop",{method:"POST"}),ze(Date.now()),k==="camera"&&Se("file")}catch{}},Rs=()=>{pt(!1),ft(null),bt(a=>(a&&URL.revokeObjectURL(a),null)),ze(Date.now())},Ms=a=>{R(i=>{const d=new Set(i);return d.has(a)?d.delete(a):d.add(a),d})},ka=async a=>{if(!window.confirm(e("delete.uploadFirst",{name:a})))return;let i=0;try{i=(await tn(a)).count}catch{i=0}if(i>0){if(!window.confirm(e("delete.uploadCascade",{n:i})))return}else if(!window.confirm(e("delete.uploadSecond")))return;U(!0),y(null);try{await Gs(a,{deleteExperiments:i>0}),await ee(),c===a&&g(null)}catch(d){y(String(d))}finally{U(!1)}},Cs=async a=>{if(window.confirm(e("delete.experimentFirst"))&&window.confirm(e("delete.experimentSecond"))){U(!0),y(null);try{await Ys(a),ta===a&&aa(null);const i=await zt(je,Ae,rt);ct(i)}catch(i){y(String(i))}finally{U(!1)}}},tt=a=>ut(Math.min(4,Math.max(.5,a))),Is=Math.max(1,Math.ceil(((Ue==null?void 0:Ue.total)??0)/rt)),he=l.useMemo(()=>n==="lab_image"?Be.filter(a=>a.type==="image"||Za(a.name)):n==="lab_video"?Be.filter(a=>a.type==="video"||ve(a.name)):Be,[Be,n]),Le=Math.max(1,Math.ceil(he.length/Kt)),Ls=l.useMemo(()=>{const a=(Ne-1)*Kt;return he.slice(a,a+Kt)},[he,Ne]);l.useEffect(()=>{_e(1)},[n]),l.useEffect(()=>{_e(a=>Math.min(a,Le))},[Le]),l.useEffect(()=>{H&&!he.some(a=>a.name===H)&&ra(null)},[he,H]);const Ea=l.useMemo(()=>jn(vt,s),[vt,s]),Ds=l.useMemo(()=>n==="lab_image"?o.filter(a=>Za(a.filename)):n==="lab_video"?o.filter(a=>ve(a.filename)):o,[o,n]),b=l.useMemo(()=>as(te),[te]),Ra=l.useMemo(()=>ss(b.prob),[b.prob]);return l.useEffect(()=>{wt(!1)},[S]),l.useEffect(()=>{if(n!=="lab_video")return;let a;const i=()=>{ln().then(hs).catch(()=>{})};return i(),a=setInterval(i,1500),()=>{a&&clearInterval(a)}},[n]),l.useEffect(()=>{(n!=="lab_video"||k!=="camera")&&(et(),pt(!1),ft(null),bt(a=>(a&&URL.revokeObjectURL(a),null))),(n!=="lab_video"||k!=="file")&&Qe()},[n,k]),l.useEffect(()=>{c||Qe()},[c]),l.useEffect(()=>()=>{et(),Qe()},[]),t.jsxs("div",{className:"flex min-h-full flex-col bg-surface text-on-surface",children:[t.jsx(Sn,{view:n,setView:r}),t.jsxs("div",{className:"flex min-h-0 flex-1",children:[t.jsxs("aside",{className:"w-64 shrink-0 border-r border-outline-variant/15 bg-surface-container-lowest p-3 text-xs",children:[t.jsx("div",{className:"mb-3 font-semibold text-on-surface-variant",children:e("sidebar.assets")}),t.jsxs("label",{className:"mb-3 flex cursor-pointer items-center gap-2 rounded bg-primary-container/30 px-2 py-2 text-on-primary-container",children:[t.jsx(Ws,{className:"h-4 w-4"}),t.jsx("span",{children:e("sidebar.upload")}),t.jsx("input",{type:"file",accept:"image/*,video/*",className:"hidden",onChange:async a=>{var d;const i=(d=a.target.files)==null?void 0:d[0];if(i){U(!0),y(null);try{const p=await Ut(i);await ee(),g(p.filename)}catch(p){y(String(p))}finally{U(!1),a.target.value=""}}}})]}),t.jsxs("button",{type:"button",className:"mb-2 flex w-full items-center gap-1 text-left text-on-surface-variant hover:text-on-surface",onClick:()=>{ee().catch(a=>y(String(a))),_e(1),Tt()},children:[t.jsx(Us,{className:"h-3 w-3"})," ",e("sidebar.refresh")]}),t.jsx("div",{className:"og-scrollbar max-h-36 overflow-y-auto border-t border-outline-variant/10 pt-2",children:Ds.map(a=>t.jsxs("div",{className:`mb-1 flex items-center gap-0.5 rounded px-1 ${c===a.filename?"bg-surface-container":""}`,children:[t.jsxs("button",{type:"button",className:`min-w-0 flex-1 truncate rounded px-1 py-1 text-left ${c===a.filename?"text-primary":""}`,title:a.filename,onClick:()=>{g(a.filename),r(ve(a.filename)?"lab_video":"lab_image")},children:[t.jsx("span",{className:"block truncate",children:nt(a.filename)}),t.jsx("span",{className:"block text-[9px] text-on-surface-variant/90",children:ve(a.filename)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")})]}),t.jsx("button",{type:"button",className:"shrink-0 rounded p-1 text-on-surface-variant hover:bg-surface-container-high hover:text-error",title:e("pool.delete"),"aria-label":e("pool.delete"),onClick:i=>{i.stopPropagation(),ka(a.filename)},children:t.jsx(es,{className:"h-3.5 w-3.5"})})]},a.filename))}),t.jsxs("div",{className:"mt-3 border-t border-outline-variant/10 pt-3",children:[t.jsxs("div",{className:"mb-2 flex items-start justify-between gap-2",children:[t.jsx("div",{className:"min-w-0 font-semibold leading-tight text-on-surface-variant",children:e("sidebar.debugCaptures")}),t.jsx("button",{type:"button",className:"shrink-0 rounded bg-primary px-2 py-1 text-[10px] font-medium text-on-primary disabled:opacity-40",disabled:!H||ya,title:H??void 0,onClick:()=>void Ss(),children:H&&/\.avi$/i.test(H)?e("sidebar.importToPoolWithTranscode"):e("sidebar.importToPoolDirect")})]}),kt!=null&&t.jsxs("div",{className:"mb-2 rounded border border-outline-variant/25 bg-surface-container-low px-2 py-2",children:[t.jsxs("div",{className:"flex items-center justify-between gap-2 text-[10px]",children:[t.jsx("span",{className:"text-on-surface-variant",children:ps||e("sidebar.flowPreparing")}),t.jsxs("span",{className:"font-mono text-on-surface",children:[Math.round((kt||0)*100),"%"]})]}),t.jsx("div",{className:"mt-1.5 h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round((kt||0)*100)}%`}})}),Et&&t.jsx("div",{className:"mt-1 text-[10px] text-on-surface-variant",title:Et,children:Et})]}),he.length===0?t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("sidebar.debugEmpty")}):t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"og-scrollbar max-h-[min(22rem,55vh)] space-y-1.5 overflow-y-auto pr-0.5",children:Ls.map(a=>t.jsxs("button",{type:"button",className:`flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left transition-colors ${H===a.name?"border-primary bg-primary-container/20":"border-outline-variant/25 hover:bg-surface-container"}`,onClick:()=>ra(a.name),children:[a.type==="image"?t.jsx("img",{src:Qs(a.name),alt:"",className:"h-11 w-11 shrink-0 rounded object-cover"}):t.jsx("div",{className:"flex h-11 w-11 shrink-0 items-center justify-center rounded bg-surface-container-high text-[9px] text-on-surface-variant",children:"video"}),t.jsxs("div",{className:"min-w-0 flex-1",children:[t.jsx("div",{className:"truncate font-mono text-[10px] text-on-surface",title:a.name,children:nt(a.name)}),t.jsxs("div",{className:"text-[9px] text-on-surface-variant",children:[a.type==="video"||ve(a.name)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")," ","· ",Pe(a.modified,s)," · ",Jt(a.size)]})]})]},a.name))}),t.jsxs("div",{className:"mt-2 flex items-center justify-between gap-1 text-[10px] text-on-surface-variant",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:Ne<=1,onClick:()=>_e(a=>Math.max(1,a-1)),children:e("history.prev")}),t.jsx("span",{className:"tabular-nums",children:e("sidebar.debugPage",{cur:Ne,total:Le})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:Ne>=Le,onClick:()=>_e(a=>Math.min(Le,a+1)),children:e("history.next")})]})]})]})]}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1",children:[t.jsxs("main",{className:"og-scrollbar min-h-0 min-w-0 flex-1 overflow-y-auto p-4",children:[n==="lab_video"&&t.jsxs("div",{className:"mb-3 max-w-5xl rounded-lg border border-outline-variant/25 bg-surface-container-low/80 p-3 text-[11px] leading-relaxed text-on-surface",children:[t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.videoLiveIntro")}),t.jsx("p",{className:"mt-1.5 text-[10px] leading-snug text-on-surface-variant/95",children:e("lab.videoMjpegHint")}),t.jsxs("div",{className:"mt-2 flex flex-wrap gap-2",children:[t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${k==="file"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("file"),children:e("lab.previewModeFile")}),t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${k==="camera"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("camera"),children:e("lab.previewModeCamera")}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-[11px] font-medium text-on-surface",onClick:()=>void Es(),children:e("lab.stopCameraPreview")})]})]}),t.jsxs("div",{className:"relative aspect-video w-full max-w-5xl overflow-hidden rounded-lg border border-outline-variant/20 bg-surface-container-lowest",children:[Ra!=null&&t.jsxs("div",{className:"pointer-events-none absolute right-2 top-2 z-20 rounded border border-white/30 bg-black/55 px-2 py-1 text-[10px] font-semibold tabular-nums text-white",children:[e("lab.previewConfidence"),": ",Ra.toFixed(1),"%"]}),n==="lab_video"&&k==="camera"?t.jsx("div",{className:"relative flex h-full min-h-[220px] flex-col items-center justify-center gap-3 bg-black p-2",children:t.jsxs("div",{className:"relative inline-block max-h-[70vh] max-w-full",children:[q&&ht?t.jsx("img",{ref:Ie,src:ht,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ca({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight})}):q&&!ht?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(at,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):mt==="busy"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.videoMjpegHint")})}):mt==="fail"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.cameraStreamStatusFail")})}):mt==="unknown"?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(at,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):t.jsx("img",{ref:Ie,src:`/api/dev/debug/camera/stream?t=${da}`,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ca({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight}),onError:()=>{(async()=>{const a=await Qa();if(a==="busy"){K("busy");return}if(a==="fail"){K("fail");return}if(Ct.current>=6){K("fail");return}Ct.current+=1,window.setTimeout(()=>ze(Date.now()),400)})()}}),t.jsx("canvas",{ref:fe,className:"pointer-events-none absolute left-0 top-0"})]})}):c?t.jsx("div",{className:"og-scrollbar relative h-full min-h-[200px] overflow-auto",children:n==="lab_video"?t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${He?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ia(a=>!a),children:[t.jsx(Aa,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>tt(me-.25),children:t.jsx(Ha,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>tt(me+.25),children:t.jsx(Ba,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>ut(1),children:t.jsx(Ua,{className:"inline h-3 w-3"})})]}),t.jsxs("div",{className:"flex min-h-[200px] flex-col items-center justify-center gap-2 bg-black py-2",children:[t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${me})`},children:t.jsxs("div",{className:"relative inline-block",children:[He&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("video",{ref:Ce,src:Ze,loop:!0,playsInline:!0,autoPlay:!0,muted:!0,preload:"metadata",className:"max-h-[70vh] w-full max-w-full object-contain",onError:()=>xt(e("lab.videoPreviewFailed")),onLoadedData:()=>{xt(null);const a=Ce.current;a&&a.play().catch(()=>{})}}),t.jsx("canvas",{ref:fe,className:"pointer-events-none absolute left-0 top-0"})]})}),ke&&t.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-1.5 text-[11px] text-error",children:ke}),Sa&&t.jsxs("div",{className:"max-w-3xl rounded border border-primary/40 bg-primary/10 px-3 py-2 text-[11px] text-on-surface",children:[t.jsx("div",{className:"font-semibold text-primary",children:e("lab.transcode.title")}),t.jsx("div",{className:"mt-1 text-on-surface-variant",children:e("lab.transcode.desc")}),ga!=null&&t.jsxs("div",{className:"mt-2",children:[t.jsx("div",{className:"mb-1 text-[10px] text-on-surface-variant",children:xs||e("lab.transcode.running")}),t.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round(ga*100)}%`}})})]}),t.jsx("div",{className:"mt-2",children:t.jsx("button",{type:"button",className:"rounded bg-primary px-3 py-1.5 text-[11px] font-medium text-on-primary disabled:opacity-50",disabled:ba,onClick:()=>void Ns(),children:e("lab.transcode.button")})})]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${He?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ia(a=>!a),children:[t.jsx(Aa,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>tt(me-.25),children:t.jsx(Ha,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>tt(me+.25),children:t.jsx(Ba,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>ut(1),children:t.jsx(Ua,{className:"inline h-3 w-3"})})]}),t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${me})`},children:t.jsxs("div",{className:"relative inline-block",children:[He&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("img",{ref:Mt,src:Ze,alt:"preview",className:"max-h-[70vh] w-full object-contain"}),t.jsx("canvas",{ref:fe,className:"pointer-events-none absolute left-0 top-0"})]})})]})}):t.jsx("div",{className:"flex h-full items-center justify-center px-4 text-center text-on-surface-variant",children:e(n==="lab_video"?"lab.selectOrUploadVideo":"lab.selectOrUpload")}),B&&t.jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/40",children:t.jsx(at,{className:"h-8 w-8 animate-spin text-primary"})})]}),(c&&n==="lab_image"||n==="lab_video"&&(k==="file"&&c||k==="camera"))&&t.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 px-3 py-2 text-xs text-on-surface",children:[t.jsx("span",{className:"shrink-0 font-medium text-on-surface-variant",children:e("lab.layers")}),t.jsx("div",{className:"flex flex-wrap gap-x-4 gap-y-1",children:["matched","pattern","all","rejected"].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-1",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:G[a],onChange:i=>is(d=>({...d,[a]:i.target.checked}))}),t.jsx("span",{children:e(a==="matched"?"lab.layer.matched":a==="pattern"?"lab.layer.pattern":a==="all"?"lab.layer.all":"lab.layer.rejected")})]},a))})]}),(c||n==="lab_video"&&k==="camera")&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 grid gap-2 sm:grid-cols-2",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.solveSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:te?t.jsxs("div",{className:"space-y-1 text-on-surface",children:[b.tSolveMs!=null&&t.jsxs("div",{className:"space-y-0.5",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveComputeMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[b.tSolveMs.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveComputeHelp")})]}),jt!=null&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveRoundTripMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[jt.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveRoundTripHelp")})]}),(b.tBackendTotalMs!=null||b.tOpenDecodeMs!=null||b.tPreprocessMs!=null||b.tExtractMs!=null||b.tSolveMs!=null)&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.backendTotalMs")}),t.jsx("span",{className:"font-mono tabular-nums",children:b.tBackendTotalMs!=null?`${b.tBackendTotalMs.toFixed(0)} ms`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[8px] text-on-surface-variant/90",children:[t.jsxs("span",{children:[e("lab.metric.openDecodeMs"),":"," ",b.tOpenDecodeMs!=null?`${b.tOpenDecodeMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.preprocessMs"),":"," ",b.tPreprocessMs!=null?`${b.tPreprocessMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.extractMs"),":"," ",b.tExtractMs!=null?`${b.tExtractMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.solveOnlyMs"),":"," ",b.tSolveMs!=null?`${b.tSolveMs.toFixed(0)} ms`:e("common.placeholder")]})]})]}),(b.raDeg!=null||b.decDeg!=null)&&t.jsxs("div",{className:"leading-tight",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.radec")}),t.jsxs("div",{className:"mt-0.5 font-mono text-[9px]",children:["α ",it(b.raDeg)," · δ"," ",it(b.decDeg)]})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[9px]",children:[b.matches!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.matches")})," ",t.jsx("span",{className:"font-mono",children:b.matches})]}),b.rmseArcsec!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.rmse")})," ",t.jsxs("span",{className:"font-mono",children:[b.rmseArcsec.toFixed(2),"″"]})]}),b.prob!=null&&t.jsxs("span",{className:"inline-flex max-w-full flex-col gap-0.5",children:[t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.prob")})," ",t.jsx("span",{className:"font-mono",children:ge(b.prob,te??void 0).line})]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probHelp")}),ge(b.prob,te??void 0).rawLine&&t.jsxs(t.Fragment,{children:[t.jsxs("span",{className:"text-[8px] text-on-surface-variant",children:[e("lab.metric.probRaw"),":"," ",ge(b.prob,te??void 0).rawLine]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probRawHelp")})]})]})]}),b.status&&t.jsxs("div",{className:"border-t border-outline-variant/15 pt-1 text-[9px]",children:[t.jsxs("span",{className:"text-on-surface-variant",children:[e("lab.metric.status")," "]}),t.jsx("span",{className:"font-mono text-secondary",children:b.status})]})]}):t.jsx("p",{className:"text-on-surface-variant",children:e("common.placeholder")})})]}),t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.imageSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.resolution")}),t.jsx("span",{className:"font-mono tabular-nums",children:At.w>0?`${At.w}×${At.h}`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.starsDetected")}),t.jsx("span",{className:"font-mono tabular-nums",children:Na??e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.fwhm")}),t.jsx("span",{children:e("common.placeholder")})]})]})]})]}),c&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 flex flex-wrap gap-4 text-xs text-on-surface-variant",children:[t.jsxs("span",{children:[e("lab.file"),":"," ",t.jsx("span",{className:"font-mono text-on-surface",title:c,children:nt(c,22,18)})]}),((Ma=o.find(a=>a.filename===c))==null?void 0:Ma.source)&&t.jsxs("span",{className:"rounded bg-surface-container-high px-2 py-0.5",children:[e("lab.source"),":"," ",(Ca=o.find(a=>a.filename===c))==null?void 0:Ca.source]})]}),t.jsxs("details",{open:!0,className:"mt-2 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 text-xs shadow-sm",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center gap-2 px-3 py-2 font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.meta.title"),gt&&t.jsx(at,{className:"h-3.5 w-3.5 animate-spin"}),t.jsx(Fe,{className:"ml-auto h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:Ea.length>0?t.jsx("dl",{className:"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-3",children:Ea.map(a=>t.jsxs("div",{children:[t.jsx("dt",{className:"text-[10px] text-on-surface-variant",children:e(a.key)}),t.jsx("dd",{className:"text-[11px] font-medium text-on-surface",children:a.value})]},`${a.key}-${a.value}`))}):vt&&!gt?t.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:e("lab.meta.partial")}):gt?null:t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.meta.noSidecar")})})]})]})]}),(c||n==="lab_video"&&k==="camera")&&(Te.length>0||S||T)&&t.jsxs(t.Fragment,{children:[Te.length>0&&t.jsxs("section",{className:"mt-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.solveHistoryTitle")}),t.jsxs("span",{className:"ml-2 normal-case text-on-surface-variant/70",children:["(",Te.length,"/",Ya,")"]})]}),t.jsx("div",{className:"og-scrollbar max-h-[min(36vh,18rem)] space-y-2 overflow-y-auto p-3",children:Te.map(a=>{var $;const i=a.batch,d=a.single,p=Pe(new Date(a.at).toISOString(),s),w=i&&i.results.length?e("results.historyBatch",{count:i.results.length}):e("results.historySingle");return t.jsxs("details",{className:"rounded border border-outline-variant/20 bg-surface-container/80 text-xs",children:[t.jsx("summary",{className:"cursor-pointer list-none px-3 py-2 font-medium text-on-surface [&::-webkit-details-marker]:hidden",children:t.jsxs("span",{children:[p," · ",w]})}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[($=i==null?void 0:i.results)!=null&&$.length?t.jsx("div",{className:"og-scrollbar flex gap-2 overflow-x-auto pb-1",children:i.results.map((E,D)=>{const M=E.result,oe=E.success===!0;return t.jsxs("div",{className:"min-w-[12rem] max-w-xs shrink-0 rounded border border-outline-variant/20 bg-surface-container-lowest p-2",children:[t.jsx("div",{className:"border-b border-outline-variant/10 pb-1 text-[10px] font-semibold text-secondary",children:String(E.label??D)}),oe&&M?t.jsx(st,{result:M,t:e,roundTripMs:null}):t.jsx("div",{className:"mt-1 text-[10px] text-error",children:String(E.error??"—")})]},D)})}):null,d&&d.result?t.jsx("div",{className:"mt-2 rounded border border-outline-variant/15 p-2",children:t.jsx(st,{result:d.result,t:e,roundTripMs:null})}):null,t.jsxs("details",{className:"mt-2",children:[t.jsx("summary",{className:"cursor-pointer text-[10px] text-primary hover:underline",children:e("results.viewRaw")}),t.jsx("pre",{className:"og-scrollbar mt-1 max-h-32 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify({single:d,batch:i},null,2)})]})]})]},a.id)})})]}),(S||T)&&t.jsxs("section",{className:"mt-3 max-h-[min(50vh,28rem)] rounded-lg border border-outline-variant/25 bg-surface-container-lowest/95",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center justify-between border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.title")}),t.jsxs("div",{className:"flex gap-3",children:[(Ia=T==null?void 0:T.results)!=null&&Ia.length?t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!(!c||!T)){for(const a of T.results){if(!a.success)continue;const i=a.result;await Ht({input_name:c,preset_label:String(a.label),result_json:a,metrics:Gt(i??null),replay:{layers:G,params:x}})}y(null)}},children:e("results.saveBatchAll")}):null,S&&t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!c&&ms!=="camera")return;const a=S.result;await Ht({input_name:c??e("lab.cameraSnapshotName"),preset_label:"manual",result_json:S,metrics:Gt(a??null),replay:{layers:G,params:x}}),y(null)},children:e("results.saveCurrent")})]})]}),t.jsx("div",{className:"og-scrollbar min-h-0 overflow-y-auto p-3",children:t.jsxs("div",{className:"og-scrollbar flex gap-3 overflow-x-auto text-xs",children:[T==null?void 0:T.results.map((a,i)=>{const d=a.result,p=us[i]??!1;return t.jsxs("div",{className:"min-w-[15rem] max-w-sm shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 font-semibold text-secondary",children:String(a.label)}),a.success?t.jsxs(t.Fragment,{children:[t.jsx(st,{result:d,t:e,roundTripMs:null}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>yt(w=>({...w,[i]:!p})),children:e(p?"results.hideRaw":"results.viewRaw")}),p&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-40 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(a,null,2)})]}):t.jsx("div",{className:"mt-2 text-[10px] text-error",children:String(a.error)}),a.success===!0&&c?t.jsx("button",{type:"button",className:"mt-3 w-full rounded bg-surface-container-high py-1.5 text-[10px] font-medium",onClick:()=>Ht({input_name:c,preset_label:String(a.label),result_json:a,metrics:Gt(d??null),replay:{layers:G,params:x}}).catch(w=>y(String(w))),children:e("results.saveRow")}):null]},i)}),S&&!T&&t.jsxs("div",{className:"min-w-[16rem] max-w-md shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 text-[10px] font-semibold uppercase text-on-surface-variant",children:e("results.title")}),t.jsx(st,{result:S.result,t:e,roundTripMs:jt}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>wt(a=>!a),children:e(fa?"results.hideRaw":"results.viewRaw")}),fa&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-48 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(S,null,2)})]})]})})]})]})]}),t.jsxs("aside",{className:"flex w-80 shrink-0 flex-col border-l border-outline-variant/15 bg-surface-container-low text-xs min-h-0",children:[n!=="lab_video"&&t.jsxs("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container",onClick:vs,disabled:B,children:e("btn.solveOne")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary",onClick:gs,disabled:B,children:e("btn.solveBatch")})]}),n==="lab_video"&&t.jsx("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:k==="file"?t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>js(),disabled:B||!_a||pe,children:e("lab.solveFileStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>Qe(),disabled:!pe,children:e("lab.solveFileStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void ws(),disabled:B||!_a||pe,children:e("lab.solveCurrentFrame")})]}):t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>ks(),disabled:B||Ee||q,children:e("lab.solveCameraStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>et(),disabled:!Ee,children:e("lab.solveCameraStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void ys(),disabled:B||q,children:e("lab.solveCameraFrame")}),t.jsxs("label",{className:"flex items-center gap-2 rounded border border-outline-variant/25 px-2 py-1.5 text-[11px]",children:[t.jsx("input",{type:"checkbox",checked:xa,onChange:a=>cs(a.target.checked)}),t.jsx("span",{children:"自动保持(解算成功后冻结) / Auto Hold"})]}),q&&t.jsx("button",{type:"button",className:"w-full rounded bg-tertiary/80 py-2.5 font-semibold text-on-tertiary disabled:opacity-40",onClick:()=>Rs(),children:"继续实时 / Resume Live"}),q&&t.jsxs("div",{className:"text-[10px] text-on-surface-variant",children:["已冻结帧 / Frozen frame ",ds??"-"]})]})}),n==="lab_video"&&Ve&&t.jsxs("div",{className:"shrink-0 border-b border-outline-variant/20 px-4 py-2 text-[10px] text-on-surface-variant",children:[t.jsx("div",{className:"font-medium text-on-surface",children:e("lab.systemLoad")}),t.jsxs("div",{children:["CPU ",Ve.cpu_usage,"% · RAM ",Ve.memory_usage,"% ·"," ",Ve.temperature,"°C"]})]}),t.jsxs("div",{className:"og-scrollbar min-h-0 flex-1 overflow-y-auto p-4",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.title"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockSolveIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.solveProfile")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.solveProfileHelp")}),t.jsxs("select",{className:"w-full rounded bg-surface-container-highest px-2 py-1",value:x.solve_profile??"balanced",onChange:a=>h(i=>({...i,solve_profile:a.target.value})),children:[t.jsx("option",{value:"speed",children:e("params.solveProfileSpeed")}),t.jsx("option",{value:"balanced",children:e("params.solveProfileBalanced")}),t.jsx("option",{value:"robust",children:e("params.solveProfileRobust")})]})]}),t.jsx(W,{label:e("params.fov"),helpKey:"params.fovHelp",value:x.fov_estimate??"",onChange:a=>h(i=>({...i,fov_estimate:a}))}),t.jsx(W,{label:e("params.fovErr"),helpKey:"params.fovErrHelp",value:x.fov_max_error??"",onChange:a=>h(i=>({...i,fov_max_error:a}))}),t.jsx(W,{label:e("params.timeout"),helpKey:"params.timeoutHelp",value:x.solve_timeout_ms??"",onChange:a=>h(i=>({...i,solve_timeout_ms:a}))}),t.jsx(W,{label:e("params.solveIntervalMs"),helpKey:"params.solveIntervalMsHelp",value:_t??"",onChange:a=>ha(a==null?$t:Math.round(a))}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalBound",{min:Ot,max:Pt,effective:le})}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalIndependent")}),n==="lab_video"&&t.jsxs("label",{className:"mt-2 block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.centroidRejectionLevel")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.centroidRejectionLevelHelp")}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"1"}),t.jsx("input",{type:"range",min:1,max:5,step:1,className:"h-2 flex-1 accent-primary",value:x.centroid_rejection_level??3,onChange:a=>h(i=>({...i,centroid_rejection_level:Number(a.target.value)}))}),t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"5"}),t.jsx("span",{className:"ml-1 min-w-[1.25rem] tabular-nums text-[10px] font-semibold text-on-surface",children:x.centroid_rejection_level??3})]}),t.jsx("p",{className:"mt-0.5 text-[9px] text-on-surface-variant/90",children:e("params.centroidRejectionScale")})]}),t.jsx(W,{label:e("params.ra"),helpKey:"params.raHelp",value:x.hint_ra_deg??"",onChange:a=>h(i=>({...i,hint_ra_deg:a}))}),t.jsx(W,{label:e("params.dec"),helpKey:"params.decHelp",value:x.hint_dec_deg??"",onChange:a=>h(i=>({...i,hint_dec_deg:a}))}),t.jsx(W,{label:e("params.maxSide"),helpKey:"params.maxSideHelp",value:x.max_image_side??"",onChange:a=>h(i=>({...i,max_image_side:a}))}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:x.detail_level==="full",onChange:a=>h(i=>({...i,detail_level:a.target.checked?"full":"summary"}))}),t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.detailLevelFull")})]}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-start gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary mt-0.5",checked:!!x.large_scale_bg_subtract,onChange:a=>h(i=>({...i,large_scale_bg_subtract:a.target.checked}))}),t.jsxs("span",{children:[t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.largeScaleBg")}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.largeScaleBgHelp")})]})]})]})]})]}),t.jsxs("details",{open:!0,className:"mt-3 rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.centroid"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockCentroidIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsx(W,{label:e("params.sigma"),helpKey:"params.sigmaHelp",step:.1,value:((La=x.centroid)==null?void 0:La.sigma)??"",onChange:a=>h(i=>({...i,centroid:{...i.centroid,sigma:a}}))}),t.jsx(W,{label:e("params.maxArea"),helpKey:"params.maxAreaHelp",value:((Da=x.centroid)==null?void 0:Da.max_area)??"",onChange:a=>h(i=>({...i,centroid:{...i.centroid,max_area:a}}))}),t.jsx(W,{label:e("params.minArea"),helpKey:"params.minAreaHelp",value:((Fa=x.centroid)==null?void 0:Fa.min_area)??"",onChange:a=>h(i=>({...i,centroid:{...i.centroid,min_area:a}}))}),t.jsx(W,{label:e("params.filtsize"),helpKey:"params.filtsizeHelp",step:2,value:((Oa=x.centroid)==null?void 0:Oa.filtsize)??"",onChange:a=>h(i=>({...i,centroid:{...i.centroid,filtsize:a}}))})]})]})]}),t.jsxs("div",{className:"mt-4 rounded-md border border-outline-variant/20 bg-surface-container-highest/40 p-3",children:[t.jsx("div",{className:"mb-2 font-semibold text-on-surface-variant",children:e("sidebar.batchPresets")}),t.jsx("p",{className:"mb-2 text-[10px] leading-snug text-on-surface-variant",children:e("sidebar.batchHint")}),t.jsx("div",{className:"og-scrollbar max-h-36 space-y-1.5 overflow-y-auto",children:[...N,...C].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",checked:I.has(a.id),onChange:()=>Ms(a.id)}),t.jsx("span",{className:"truncate",children:a.name})]},a.id))})]}),t.jsxs("div",{className:"mt-6 border-t border-outline-variant/20 pt-4",children:[t.jsx("div",{className:"mb-2 font-semibold",children:e("btn.applyPresets")}),t.jsx("div",{className:"og-scrollbar max-h-24 space-y-1 overflow-y-auto",children:[...N,...C].map(a=>t.jsx("button",{type:"button",className:"block w-full truncate text-left text-primary hover:underline",onClick:()=>bs(a.params),children:a.name},a.id))}),t.jsxs("div",{className:"mt-3 flex gap-1",children:[t.jsx("input",{className:"flex-1 rounded bg-surface-container-highest px-2 py-1",placeholder:e("placeholder.newPreset"),value:dt,onChange:a=>sa(a.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container-high px-2",onClick:async()=>{if(dt.trim()){U(!0);try{await Zs(dt.trim(),x),sa(""),await ee();const a=await Bt("user");v(a.presets)}catch(a){y(String(a))}finally{U(!1)}}},children:e("btn.savePreset")})]})]})]})]})]}),n==="pool"&&t.jsx(En,{uploads:o,onDelete:a=>void ka(a)}),n==="history"&&t.jsx(kn,{historyQ:je,setHistoryQ:rs,historyPage:Ae,setHistoryPage:ea,historyData:Ue,historyExpandId:ta,setHistoryExpandId:aa,historyTotalPages:Is,onDeleteExperiment:a=>void Cs(a),onSearch:()=>zt(je,1,rt).then(a=>{ea(1),ct(a)})})]}),t.jsx("div",{className:"pointer-events-none fixed bottom-4 right-4 z-[100] flex max-w-sm flex-col gap-2",children:ns.map(a=>t.jsx("div",{className:`pointer-events-auto rounded border px-3 py-2 text-left text-xs shadow-lg ${a.variant==="error"?"border-error/50 bg-error-container/95 text-on-error-container":a.variant==="warn"?"border-amber-500/40 bg-surface-container-highest/95 text-on-surface":"border-outline-variant/40 bg-surface-container-high/95 text-on-surface"}`,children:a.text},a.id))})]})}$s.createRoot(document.getElementById("root")).render(t.jsx(Ts.StrictMode,{children:t.jsx(As,{children:t.jsx(Mn,{})})})); diff --git a/web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js b/web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js new file mode 100644 index 0000000..0854fa4 --- /dev/null +++ b/web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js @@ -0,0 +1,51 @@ +var As=Object.defineProperty;var Ua=e=>{throw TypeError(e)};var Us=(e,s,n)=>s in e?As(e,s,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[s]=n;var O=(e,s,n)=>Us(e,typeof s!="symbol"?s+"":s,n),Ba=(e,s,n)=>s.has(e)||Ua("Cannot "+n);var h=(e,s,n)=>(Ba(e,s,"read from private field"),n?n.call(e):s.get(e)),te=(e,s,n)=>s.has(e)?Ua("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(e):s.set(e,n),Oe=(e,s,n,r)=>(Ba(e,s,"write to private field"),r?r.call(e,n):s.set(e,n),n);import{j as t,r as i,a as Bs,R as Hs}from"./client-D1ZVDB-N.js";import{c as q,u as Te,T as ss,I as zs}from"./index-CV2LaaT8.js";import{R as Ws}from"./refresh-cw-CIJtimxg.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 Fe=q("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + * @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 Vs=q("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** + * @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 Ha=q("Grid3x3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M3 9h18",key:"1pudct"}],["path",{d:"M3 15h18",key:"5xshup"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** + * @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 Gs=q("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/** + * @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 Ks=q("House",[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",key:"5wwlr5"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"1d0kgt"}]]);/** + * @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 st=q("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** + * @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 za=q("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);/** + * @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 qs=q("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** + * @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 Wa=q("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]);/** + * @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 Va=q("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),_="/api/dev";async function W(e){if((e.headers.get("content-type")||"").includes("application/json"))return e.json();const n=await e.text();throw new Error(n||`HTTP ${e.status}`)}async function Js(){const e=await fetch(`${_}/analysis/uploads`);if(!e.ok)throw new Error(await e.text());return e.json()}async function Zs(e,s){const n=new URLSearchParams;s!=null&&s.deleteExperiments&&n.set("delete_experiments","true");const r=n.toString(),o=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}${r?`?${r}`:""}`,{method:"DELETE"});if(!o.ok)throw new Error(await o.text());return await o.json().catch(()=>({}))}async function Bt(e,s="analysis_upload"){const n=new FormData;n.append("file",e),n.append("source",s);const r=await fetch(`${_}/analysis/upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}async function Ys(e){const s=await fetch(`${_}/analysis/uploads/import_from_debug`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:e})}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Ga(e){const s=await fetch(`${_}/analysis/uploads/replace_video`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Xs(e,s){const n=await fetch(`${_}/analysis/solve/image`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,...s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Qs(e,s){const n=await fetch(`${_}/analysis/solve/batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,runs:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Ht(e){const s=await fetch(`${_}/analysis/presets?scope=${e}`);if(!s.ok)throw new Error(await s.text());return s.json()}async function en(e,s){const n=await fetch(`${_}/analysis/presets`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,params:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function zt(e){const s=await fetch(`${_}/analysis/experiments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function tn(e){const s=await fetch(`${_}/analysis/experiments/${encodeURIComponent(e)}`,{method:"DELETE"});if(!s.ok)throw new Error(await s.text())}async function Wt(e,s,n=30){const r=new URLSearchParams({page:String(s),page_size:String(n)});e&&r.set("q",e);const o=await fetch(`${_}/analysis/experiments?${r}`);if(!o.ok)throw new Error(await o.text());return o.json()}async function an(){const e=await fetch(`${_}/debug/files`);if(!e.ok)throw new Error(await e.text());return e.json()}function sn(e){return`${_}/debug/files/${encodeURIComponent(e)}`}async function Ka(e){const s=await fetch(`${_}/debug/files/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function nn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}function Vt(e){return`${_}/analysis/uploads/file?filename=${encodeURIComponent(e)}`}async function qa(e){const s=await fetch(`${_}/analysis/experiments/export?format=${e}`);if(!s.ok)throw new Error(await s.text());return s.text()}async function rn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/experiment_count`);if(!s.ok)throw new Error(await s.text());return s.json()}async function ln(){const e=await fetch(`${_}/analysis/settings`);if(!e.ok)throw new Error(await e.text());return e.json()}async function on(e){const s=await fetch(`${_}/analysis/solve/frame`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function cn(e,s){const n=new FormData;n.append("file",e,"frame.jpg"),n.append("payload",JSON.stringify(s));const r=await fetch(`${_}/analysis/solve/frame_upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}function dn(e){return`${_}/analysis/experiments/${encodeURIComponent(e)}/asset`}async function un(){const e=await fetch("/api/dev/system/info");if(!e.ok)throw new Error(await e.text());return e.json()}function ns(e,s,n,r,o){var g,y,R,d;if(!r)return;if(e.clearRect(0,0,s,n),o.all&&Array.isArray(r.stars_all_centroids)){e.fillStyle="rgba(156, 163, 175, 0.85)";for(const m of r.stars_all_centroids)e.beginPath(),e.arc(m.x,m.y,2.4,0,Math.PI*2),e.fill()}if(o.rejected&&Array.isArray(r.stars_rejected_centroids)){e.strokeStyle="rgba(248, 113, 113, 0.95)",e.lineWidth=1.6;for(const m of r.stars_rejected_centroids)e.beginPath(),e.moveTo(m.x-4,m.y-4),e.lineTo(m.x+4,m.y+4),e.moveTo(m.x+4,m.y-4),e.lineTo(m.x-4,m.y+4),e.stroke()}if(o.pattern&&Array.isArray(r.stars_pattern)){e.strokeStyle="rgba(251, 146, 60, 0.95)",e.lineWidth=2;for(const m of r.stars_pattern)e.beginPath(),e.arc(m.x,m.y,6,0,Math.PI*2),e.stroke()}if(o.matched&&Array.isArray(r.stars_matched)){e.strokeStyle="rgba(34, 197, 94, 0.95)",e.fillStyle="rgba(34, 197, 94, 0.95)",e.lineWidth=2,e.font="11px system-ui, sans-serif";for(const m of r.stars_matched)e.beginPath(),e.arc(m.x,m.y,7,0,Math.PI*2),e.stroke(),m.mag!=null&&e.fillText(`m${Number(m.mag).toFixed(1)}`,m.x+4,m.y-4)}const u=r.overlay_ext;if(Array.isArray(u==null?void 0:u.labels_topn)&&u.labels_topn.length>0){e.fillStyle="rgba(96, 165, 250, 0.95)",e.font="12px system-ui, sans-serif";for(const m of u.labels_topn){if(typeof(m==null?void 0:m.x)!="number"||typeof(m==null?void 0:m.y)!="number")continue;const b=typeof m.mag=="number"?` m${m.mag.toFixed(1)}`:"",x=`${m.name??"Star"}${b}`;e.fillText(x,m.x+8,m.y+14)}}const p=u==null?void 0:u.polar_guide;if(p&&typeof((g=p.frame_center)==null?void 0:g.x)=="number"&&typeof((y=p.frame_center)==null?void 0:y.y)=="number"&&typeof((R=p.target)==null?void 0:R.x)=="number"&&typeof((d=p.target)==null?void 0:d.y)=="number"){const m=p.frame_center.x,b=p.frame_center.y,x=p.target.x,I=p.target.y;e.strokeStyle="rgba(244, 63, 94, 0.95)",e.fillStyle="rgba(244, 63, 94, 0.95)",e.lineWidth=2,e.beginPath(),e.moveTo(m,b),e.lineTo(x,I),e.stroke(),e.beginPath(),e.arc(x,I,6,0,Math.PI*2),e.stroke();const M=typeof p.angular_sep_deg=="number"?p.angular_sep_deg.toFixed(2):"-";e.font="12px system-ui, sans-serif",e.fillText(`Polar ${M}deg`,x+10,I-6)}}function Ja(e,s,n,r){if(!n)return;const o=s.naturalWidth||1,u=s.naturalHeight||1;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}function mn(e,s,n,r){if(!n)return;const o=s.videoWidth||1,u=s.videoHeight||1;if(o<2||u<2)return;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}var N;(function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"})(N||(N={}));const xn=(()=>{let e=0;return()=>e++})(),pn=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),fn=new Error("called FFmpeg.terminate()");var A,ae,Y,ue,me,ot,P;class hn{constructor(){te(this,A,null);te(this,ae,{});te(this,Y,{});te(this,ue,[]);te(this,me,[]);O(this,"loaded",!1);te(this,ot,()=>{h(this,A)&&(h(this,A).onmessage=({data:{id:s,type:n,data:r}})=>{switch(n){case N.LOAD:this.loaded=!0,h(this,ae)[s](r);break;case N.MOUNT:case N.UNMOUNT:case N.EXEC:case N.FFPROBE:case N.WRITE_FILE:case N.READ_FILE:case N.DELETE_FILE:case N.RENAME:case N.CREATE_DIR:case N.LIST_DIR:case N.DELETE_DIR:h(this,ae)[s](r);break;case N.LOG:h(this,ue).forEach(o=>o(r));break;case N.PROGRESS:h(this,me).forEach(o=>o(r));break;case N.ERROR:h(this,Y)[s](r);break}delete h(this,ae)[s],delete h(this,Y)[s]})});te(this,P,({type:s,data:n},r=[],o)=>h(this,A)?new Promise((u,p)=>{const g=xn();h(this,A)&&h(this,A).postMessage({id:g,type:s,data:n},r),h(this,ae)[g]=u,h(this,Y)[g]=p,o==null||o.addEventListener("abort",()=>{p(new DOMException(`Message # ${g} was aborted`,"AbortError"))},{once:!0})}):Promise.reject(pn));O(this,"load",({classWorkerURL:s,...n}={},{signal:r}={})=>(h(this,A)||(Oe(this,A,s?new Worker(new URL(s,import.meta.url),{type:"module"}):new Worker(new URL("/static/analysis-lab/assets/worker-BAOIWoxA.js",import.meta.url),{type:"module"})),h(this,ot).call(this)),h(this,P).call(this,{type:N.LOAD,data:n},void 0,r)));O(this,"exec",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.EXEC,data:{args:s,timeout:n}},void 0,r));O(this,"ffprobe",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.FFPROBE,data:{args:s,timeout:n}},void 0,r));O(this,"terminate",()=>{const s=Object.keys(h(this,Y));for(const n of s)h(this,Y)[n](fn),delete h(this,Y)[n],delete h(this,ae)[n];h(this,A)&&(h(this,A).terminate(),Oe(this,A,null),this.loaded=!1)});O(this,"writeFile",(s,n,{signal:r}={})=>{const o=[];return n instanceof Uint8Array&&o.push(n.buffer),h(this,P).call(this,{type:N.WRITE_FILE,data:{path:s,data:n}},o,r)});O(this,"mount",(s,n,r)=>{const o=[];return h(this,P).call(this,{type:N.MOUNT,data:{fsType:s,options:n,mountPoint:r}},o)});O(this,"unmount",s=>{const n=[];return h(this,P).call(this,{type:N.UNMOUNT,data:{mountPoint:s}},n)});O(this,"readFile",(s,n="binary",{signal:r}={})=>h(this,P).call(this,{type:N.READ_FILE,data:{path:s,encoding:n}},void 0,r));O(this,"deleteFile",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_FILE,data:{path:s}},void 0,n));O(this,"rename",(s,n,{signal:r}={})=>h(this,P).call(this,{type:N.RENAME,data:{oldPath:s,newPath:n}},void 0,r));O(this,"createDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.CREATE_DIR,data:{path:s}},void 0,n));O(this,"listDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.LIST_DIR,data:{path:s}},void 0,n));O(this,"deleteDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_DIR,data:{path:s}},void 0,n))}on(s,n){s==="log"?h(this,ue).push(n):s==="progress"&&h(this,me).push(n)}off(s,n){s==="log"?Oe(this,ue,h(this,ue).filter(r=>r!==n)):s==="progress"&&Oe(this,me,h(this,me).filter(r=>r!==n))}}A=new WeakMap,ae=new WeakMap,Y=new WeakMap,ue=new WeakMap,me=new WeakMap,ot=new WeakMap,P=new WeakMap;var Za;(function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"})(Za||(Za={}));const bn=new Error("failed to get response body reader"),vn=new Error("failed to complete download"),gn="Content-Length",yn=e=>new Promise((s,n)=>{const r=new FileReader;r.onload=()=>{const{result:o}=r;o instanceof ArrayBuffer?s(new Uint8Array(o)):s(new Uint8Array)},r.onerror=o=>{var u,p;n(Error(`File could not be read! Code=${((p=(u=o==null?void 0:o.target)==null?void 0:u.error)==null?void 0:p.code)||-1}`))},r.readAsArrayBuffer(e)}),wn=async e=>{let s;if(typeof e=="string")/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?s=atob(e.split(",")[1]).split("").map(n=>n.charCodeAt(0)):s=await(await fetch(e)).arrayBuffer();else if(e instanceof URL)s=await(await fetch(e)).arrayBuffer();else if(e instanceof File||e instanceof Blob)s=await yn(e);else return new Uint8Array;return new Uint8Array(s)},jn=async(e,s)=>{var o;const n=await fetch(e);let r;try{const u=parseInt(n.headers.get(gn)||"-1"),p=(o=n.body)==null?void 0:o.getReader();if(!p)throw bn;const g=[];let y=0;for(;;){const{done:m,value:b}=await p.read(),x=b?b.length:0;if(m){if(u!=-1&&u!==y)throw vn;s&&s({url:e,total:u,received:y,delta:x,done:m});break}g.push(b),y+=x,s&&s({url:e,total:u,received:y,delta:x,done:m})}const R=new Uint8Array(y);let d=0;for(const m of g)R.set(m,d),d+=m.length;r=R.buffer}catch(u){console.log("failed to send download progress event: ",u),r=await n.arrayBuffer()}return r},Ya=async(e,s,n=!1,r)=>{const o=n?await jn(e,r):await(await fetch(e)).arrayBuffer(),u=new Blob([o],{type:s});return URL.createObjectURL(u)};let Gt=null,Pe=null,F;async function Nn(){if(Gt)return Gt;if(Pe)return Pe;Pe=(async()=>{const e=new hn;e.on("progress",({progress:o})=>{F==null||F(o,"transcoding")}),F==null||F(.01,"loading_ffmpeg");const s="https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm",n=await Ya(`${s}/ffmpeg-core.js`,"text/javascript");F==null||F(.04,"loading_ffmpeg");const r=await Ya(`${s}/ffmpeg-core.wasm`,"application/wasm");return F==null||F(.07,"loading_ffmpeg"),await e.load({coreURL:n,wasmURL:r}),Gt=e,e})();try{return await Pe}finally{Pe=null}}async function _n(e){const s=URL.createObjectURL(e);try{return await new Promise(r=>{const o=document.createElement("video");o.preload="metadata",o.onloadedmetadata=()=>{const u=Number(o.duration);r(Number.isFinite(u)&&u>0?u:null)},o.onerror=()=>r(null),o.src=s})}finally{URL.revokeObjectURL(s)}}async function Xa(e,s){const n=F;F=s;try{const r=await Nn(),o="input.avi",u="output.mp4";s==null||s(.1,"writing_input"),await r.writeFile(o,await wn(e)),await r.exec(["-i",o,"-c:v","libx264","-preset","veryfast","-crf","24","-pix_fmt","yuv420p","-movflags","+faststart","-an",u]);const p=await r.readFile(u);s==null||s(.98,"packing_output");const g=new Blob([p],{type:"video/mp4"}),y=new File([g],e.name.replace(/\.avi$/i,".mp4"),{type:"video/mp4"});await r.deleteFile(o),await r.deleteFile(u);const R=await _n(y);return s==null||s(1,"done"),{file:y,duration_s:R}}finally{F=n}}function Zt(e){return e==null||Number.isNaN(e)?"—":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(2)} MB`}function $e(e,s){if(!e)return"—";try{const n=new Date(e);return new Intl.DateTimeFormat(s==="en"?"en-GB":"zh-CN",{dateStyle:"short",timeStyle:"medium"}).format(n)}catch{return e}}function de(e){return e==null?null:typeof e=="string"&&e.trim()?e.trim():typeof e=="number"&&!Number.isNaN(e)?String(e):null}function kn(e){return typeof e!="number"||e<=0?null:e>=1e6?`${(e/1e6).toFixed(2)} s`:e>=1e3?`${(e/1e3).toFixed(0)} ms`:`${e} µs`}function Sn(e,s){if(!e)return[];const n=[],r=kn(e.exposure_us);r&&n.push({key:"meta.exposure",value:r});const o=de(e.analogue_gain),u=de(e.digital_gain);if(o||u){const b=[o?`A ${o}`:"",u?`D ${u}`:""].filter(Boolean);n.push({key:"meta.gain",value:b.join(" · ")})}const p=de(e.fps);p&&n.push({key:"meta.fps",value:p});const g=de(e.sensor);g&&n.push({key:"meta.sensor",value:g});const y=de(e.color_mode);y&&n.push({key:"meta.colorMode",value:y});const R=de(e.resolution);R&&n.push({key:"meta.outputResolution",value:R});const d=de(e.modified);d&&n.push({key:"meta.fileTime",value:$e(d,s)});const m=e.size;return typeof m=="number"&&m>0&&n.push({key:"meta.fileSize",value:Zt(m)}),n}function rs(e){const s=n=>typeof n=="number"&&!Number.isNaN(n)?n:null;return e?{tSolveMs:s(e.t_solve_ms),tExtractMs:s(e.t_extract_ms),tPreprocessMs:s(e.t_preprocess_ms),tOpenDecodeMs:s(e.t_open_decode_ms),tBackendTotalMs:s(e.t_backend_total_ms),raDeg:s(e.ra_deg),decDeg:s(e.dec_deg),matches:s(e.matches),rmseArcsec:s(e.rmse_arcsec),prob:s(e.prob),status:typeof e.status=="string"?e.status:null}:{tSolveMs:null,tExtractMs:null,tPreprocessMs:null,tOpenDecodeMs:null,tBackendTotalMs:null,raDeg:null,decDeg:null,matches:null,rmseArcsec:null,prob:null,status:null}}function it(e){return e==null?"—":`${e.toFixed(4)}°`}function ls(e){if(e==null||typeof e!="number"||Number.isNaN(e)||e<0)return null;if(e>=1)return 0;if(e===0)return 100;const s=(1-e)*100;if(s<=100-1e-12)return Math.min(100,Math.max(0,s));const n=-Math.log10(Math.max(e,Number.MIN_VALUE)),r=14,u=Math.min(1,Math.max(0,(n-r)/(50-r)));return Math.min(100,Math.max(0,70+30*u))}function En(e){const s=ls(e);return s==null?"—":`${s.toFixed(1)}%`}function Rn(e){if(e==null)return"—";if(typeof e=="number"&&!Number.isNaN(e)){const s=Math.abs(e);return s>0&&s<.001?e.toExponential(4):s>=0&&s<=1?`${(e*100).toFixed(4)}%`:String(e)}return String(e)}function ye(e,s){const n=s==null?void 0:s.tetra,r=n?n.Prob??n.prob:void 0,o=En(e),u=r!=null?Rn(r):null;return{line:o,rawLine:u}}function nt({result:e,t:s,roundTripMs:n}){const r=rs(e??void 0);return e?t.jsxs("div",{className:"mt-2 space-y-2 text-[10px]",children:[t.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[r.tBackendTotalMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.backendTotalMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tBackendTotalMs.toFixed(0)," ms"]})]}),r.tSolveMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveComputeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tSolveMs.toFixed(0)," ms"]})]}),r.tOpenDecodeMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.openDecodeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tOpenDecodeMs.toFixed(0)," ms"]})]}),r.tPreprocessMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.preprocessMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tPreprocessMs.toFixed(0)," ms"]})]}),r.tExtractMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.extractMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tExtractMs.toFixed(0)," ms"]})]}),n!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveRoundTripMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[n.toFixed(0)," ms"]})]}),r.matches!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.matches")}),t.jsx("div",{className:"font-semibold tabular-nums text-on-surface",children:r.matches})]}),r.rmseArcsec!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.rmse")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.rmseArcsec.toFixed(2),"″"]})]}),r.prob!=null&&t.jsxs("div",{className:"col-span-2",children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.prob")}),t.jsx("div",{className:"font-semibold text-on-surface",children:ye(r.prob,e).line}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/90",children:s("lab.metric.probHelp")}),ye(r.prob,e).rawLine&&t.jsxs("div",{className:"mt-1 text-[9px] text-on-surface-variant",children:[s("lab.metric.probRaw"),": ",ye(r.prob,e).rawLine,t.jsx("p",{className:"mt-0.5 text-[8px] leading-snug opacity-90",children:s("lab.metric.probRawHelp")})]})]})]}),e.centroid_quality!=null&&typeof e.centroid_quality=="object"&&t.jsxs("div",{className:"rounded border border-outline-variant/25 bg-surface-container-high/40 p-2",children:[t.jsxs("div",{className:"text-[9px] font-medium text-on-surface-variant",children:[s("lab.centroidQualityTitle"),typeof e.centroid_quality.level=="number"?` · L${e.centroid_quality.level}`:""]}),Array.isArray(e.centroid_quality.hints)&&e.centroid_quality.hints.length>0?t.jsx("ul",{className:"mt-1 list-inside list-disc text-[9px] leading-snug text-on-surface-variant",children:e.centroid_quality.hints.map((o,u)=>t.jsx("li",{children:o},u))}):null,(()=>{const o=e.centroid_quality.metrics;if(!o)return null;const u=o.input_count,p=o.output_count,g=o.removed_dense,y=o.removed_line;return typeof u!="number"||typeof p!="number"||typeof g!="number"||typeof y!="number"?null:t.jsx("p",{className:"mt-1 font-mono text-[9px] text-on-surface-variant/95",children:s("lab.centroidQualityMetrics",{in:u,out:p,dense:g,line:y})})})()]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.radec")}),t.jsxs("div",{className:"font-mono text-[9px] text-on-surface",children:["α ",it(r.raDeg)," · δ ",it(r.decDeg)]})]}),r.status&&t.jsxs("div",{className:"rounded bg-surface-container-high px-2 py-1 text-[9px] font-mono text-on-surface",children:[s("lab.metric.status"),": ",r.status]})]}):t.jsx("p",{className:"mt-2 text-[10px] text-on-surface-variant",children:"—"})}function z({label:e,helpKey:s,value:n,onChange:r,type:o="number",step:u}){const{t:p}=Te(),g=s?p(s):void 0;return t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e}),g&&t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:g}),t.jsx("input",{type:o,step:u,className:"w-full rounded bg-surface-container-highest px-2 py-1",value:n===""?"":n,onChange:y=>{const R=y.target.value;r(R===""?void 0:Number(R))}})]})}function Mn({view:e,setView:s}){const{t:n,locale:r,setLocale:o}=Te();return t.jsxs("header",{className:"flex h-12 shrink-0 items-center justify-between border-b border-outline-variant/20 px-4",children:[t.jsxs("div",{className:"flex items-center gap-6",children:[t.jsx("span",{className:"font-headline text-sm font-bold tracking-wide text-on-surface",children:n("app.title")}),t.jsxs("nav",{className:"flex flex-wrap gap-3 text-xs",children:[t.jsx("button",{type:"button",className:e==="lab_image"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_image"),children:n("nav.labImage")}),t.jsx("button",{type:"button",className:e==="lab_video"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_video"),children:n("nav.labVideo")}),t.jsx("button",{type:"button",className:e==="pool"?"text-primary":"text-on-surface-variant",onClick:()=>s("pool"),children:n("nav.pool")}),t.jsx("button",{type:"button",className:e==="history"?"text-primary":"text-on-surface-variant",onClick:()=>s("history"),children:n("nav.history")})]})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("zh"),children:n("lang.zh")}),t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("en"),children:n("lang.en")})]}),t.jsxs("a",{className:"flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 text-xs text-on-surface-variant hover:bg-surface-container",href:"/debug",children:[t.jsx(Ks,{className:"h-3.5 w-3.5"})," ",n("nav.systemAdmin")]})]})]})}function Cn({historyQ:e,setHistoryQ:s,historyPage:n,setHistoryPage:r,historyData:o,historyExpandId:u,setHistoryExpandId:p,historyTotalPages:g,onDeleteExperiment:y,onSearch:R}){var b;const{t:d,locale:m}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsx("p",{className:"mb-4 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 p-3 text-xs leading-relaxed text-on-surface-variant",children:d("history.intro")}),t.jsxs("div",{className:"mb-4 flex flex-wrap items-center gap-4",children:[t.jsxs("h2",{className:"flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Gs,{className:"h-5 w-5"})," ",d("history.title")]}),t.jsx("input",{className:"rounded bg-surface-container-highest px-2 py-1 text-xs",placeholder:d("history.search"),value:e,onChange:x=>s(x.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-1 text-xs",onClick:R,children:d("history.searchBtn")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("json").then(x=>{const I=new Blob([x],{type:"application/json"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.json",M.click()}),children:d("history.exportJson")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("csv").then(x=>{const I=new Blob([x],{type:"text/csv"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.csv",M.click()}),children:d("history.exportCsv")})]}),t.jsxs("div",{className:"flex flex-wrap items-center gap-3 text-xs text-on-surface-variant",children:[t.jsx("span",{children:d("history.total",{n:(o==null?void 0:o.total)??0})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n<=1,onClick:()=>r(x=>Math.max(1,x-1)),children:d("history.prev")}),t.jsxs("span",{children:[n," / ",g]}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n>=g,onClick:()=>r(x=>x+1),children:d("history.next")})]}),t.jsx("ul",{className:"mt-2 space-y-2",children:(b=o==null?void 0:o.items)==null?void 0:b.map(x=>{const I=String(x.id??""),M=x.metrics,se=u===I;return t.jsxs("li",{className:"rounded border border-outline-variant/20 p-2 text-[11px]",children:[t.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-2 font-mono",children:[t.jsxs("div",{children:[t.jsxs("div",{className:"text-on-surface",children:[$e(String(x.created_at??""),m)," —"," ",String(x.input_name)," — ",String(x.preset_label)]}),t.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[d("history.preset"),": ",String(x.preset_label)," · ",d("history.metrics"),":"," ","matches=",String((M==null?void 0:M.matches)??"—")," rmse=",String((M==null?void 0:M.rmse_arcsec)??"—")]})]}),t.jsxs("div",{className:"flex shrink-0 gap-1",children:[t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 text-[10px]",onClick:()=>p(se?null:I),children:d(se?"history.collapse":"history.detail")}),t.jsx("button",{type:"button",className:"rounded px-2 py-0.5 text-[10px] text-error hover:bg-error-container/20",title:d("history.delete"),onClick:()=>void y(I),children:d("history.delete")})]})]}),se&&t.jsxs("div",{className:"mt-2 space-y-2",children:[x.asset_snapshot_relpath?t.jsx("img",{src:dn(I),alt:"",className:"max-h-48 max-w-full rounded border border-outline-variant/20 object-contain"}):null,t.jsx("pre",{className:"og-scrollbar max-h-64 overflow-auto rounded bg-surface-container p-2 text-[10px]",children:JSON.stringify(x.result_json,null,2)})]})]},I)})})]})}function In({uploads:e,onDelete:s}){const{t:n,locale:r}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsxs("h2",{className:"mb-4 flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Vs,{className:"h-5 w-5"})," ",n("pool.title")]}),t.jsxs("table",{className:"w-full text-left text-xs",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-outline-variant/30 text-on-surface-variant",children:[t.jsx("th",{className:"py-2",children:n("pool.col.name")}),t.jsx("th",{className:"py-2",children:n("pool.col.source")}),t.jsx("th",{className:"py-2",children:n("pool.col.size")}),t.jsx("th",{className:"py-2",children:n("pool.col.time")}),t.jsx("th",{className:"w-16 py-2 text-center",children:n("pool.delete")})]})}),t.jsx("tbody",{children:e.map(o=>t.jsxs("tr",{className:"border-b border-outline-variant/10",children:[t.jsx("td",{className:"py-2 font-mono",children:o.filename}),t.jsx("td",{className:"py-2",children:o.source??n("common.placeholder")}),t.jsx("td",{className:"py-2",children:Zt(o.size)}),t.jsx("td",{className:"py-2",children:$e(o.modified_at,r)}),t.jsx("td",{className:"py-2 text-center",children:t.jsx("button",{type:"button",className:"inline-flex rounded p-1 text-on-surface-variant hover:bg-error-container/30 hover:text-error",title:n("pool.delete"),"aria-label":n("pool.delete"),onClick:()=>void s(o.filename),children:t.jsx(ss,{className:"h-3.5 w-3.5"})})})]},o.filename))})]})]})}function Kt(e){return e?{matches:e.matches,rmse_arcsec:e.rmse_arcsec,status:e.status,prob:e.prob,t_solve_ms:e.t_solve_ms}:{}}function Ln(e){var n,r;if(!e)return null;const s=e.solve_overlay;return(n=s==null?void 0:s.stars_matched)!=null&&n.length?s.stars_matched.length:(r=s==null?void 0:s.stars_all_centroids)!=null&&r.length?s.stars_all_centroids.length:typeof e.matches=="number"?e.matches:null}function Qa(e){return/\.(jpe?g|png|webp|bmp|gif|fits?)$/i.test(e)}function ge(e){return/\.(mp4|mov|webm|mkv|avi)$/i.test(e)}function rt(e,s=16,n=14){return e.length<=s+n+1?e:`${e.slice(0,s)}...${e.slice(-n)}`}const es=10,lt=30,qt=6,Jt=()=>({hint_ra_deg:45,hint_dec_deg:80,fov_estimate:11,fov_max_error:void 0,solve_timeout_ms:1500,solve_profile:"balanced",max_image_side:1280,large_scale_bg_subtract:!1,detail_level:"summary",centroid_rejection_level:3,centroid:{sigma:2.5,max_area:400,min_area:5,filtsize:25,binary_open:!0,max_axis_ratio:void 0}});function ts(e,s,n,r){const o=typeof r=="number"&&r>0?` ${e("lab.gateNextMs",{ms:r})}`:"",u=String(n??"").toLowerCase();return s==="SKIPPED_BUSY"?u.includes("recording")?`${e("lab.gate.skipRecording")}${o}`:u.includes("previous")?`${e("lab.gate.skipBusyInFlight")}${o}`:`${e("lab.gate.skipBusyFallback",{detail:String(n??"")})}${o}`:s==="SKIPPED_INTERVAL"?`${e("lab.gate.skipInterval")}${o}`:`${s??"?"}${n?`: ${String(n)}`:""}${o}`}async function as(){try{const e=await fetch("/api/dev/debug/camera/stream/status",{cache:"no-store",credentials:"same-origin"});if(!e.ok)return"fail";const s=await e.json();if(s.success===!1)return"fail";const n=Number(s.max_clients??0),r=Number(s.active_clients??0);return n>0&&r>=n?"busy":"ok"}catch{return"fail"}}function Dn(){var La,Da,Oa,Fa,Pa,$a,Ta;const{t:e,locale:s}=Te(),[n,r]=i.useState("lab_image"),[o,u]=i.useState(!1),[p,g]=i.useState(!1),[y,R]=i.useState([]),[d,m]=i.useState(null),[b,x]=i.useState(Jt),[I,M]=i.useState([]),[se,Yt]=i.useState([]),[Xt,is]=i.useState(new Set),[ne,U]=i.useState(!1),[ct,w]=i.useState(null),[k,we]=i.useState(null),[T,je]=i.useState(null),[Ae,Qt]=i.useState([]),[os,ea]=i.useState([]),ta=i.useRef(null),aa=i.useRef(null),[Ne,cs]=i.useState(""),[Ue,sa]=i.useState(1),[Be,dt]=i.useState(null),[na,ra]=i.useState(null),[ut,la]=i.useState(""),[V,ds]=i.useState({matched:!0,pattern:!0,all:!0,rejected:!0}),[He,ia]=i.useState([]),[B,oa]=i.useState(null),[_e,ke]=i.useState(1),[ze,ca]=i.useState(!1),[xe,mt]=i.useState(1),[da,us]=i.useState({w:0,h:0}),[pe,ms]=i.useState({w:0,h:0}),[ua,ma]=i.useState({w:0,h:0}),[S,Se]=i.useState("file"),[xa,We]=i.useState(()=>Date.now()),[xt,G]=i.useState("unknown"),[Ee,pt]=i.useState(null),[Re,pa]=i.useState(!1),[fe,fa]=i.useState(!1),[ha,xs]=i.useState(!1),[K,ft]=i.useState(!1),[ps,ht]=i.useState(null),[bt,vt]=i.useState(null),[gt,Ve]=i.useState(null),[yt,ba]=i.useState(!1),[fs,wt]=i.useState({}),[va,jt]=i.useState(!1),[Nt,J]=i.useState(null),[hs,Me]=i.useState(null),[_t,Ce]=i.useState(null),[kt,ga]=i.useState(null),[ya,wa]=i.useState(!1),[ja,St]=i.useState(null),[bs,X]=i.useState(null),[Na,_a]=i.useState(!1),[Et,re]=i.useState(null),[vs,H]=i.useState(null),[Rt,Mt]=i.useState(null),Ct=i.useRef(null),Ie=i.useRef(null),Le=i.useRef(null),It=i.useRef(0),le=i.useRef(null),ie=i.useRef(null),Lt=i.useRef(!1),Dt=i.useRef(!1),Ot=i.useRef(!1),Ft=i.useRef(!1),he=i.useRef(null),gs=i.useCallback(async()=>{const a=Le.current;if(!a||a.naturalWidth<2)return null;const l=document.createElement("canvas");l.width=a.naturalWidth,l.height=a.naturalHeight;const c=l.getContext("2d");if(!c)return null;try{c.drawImage(a,0,0)}catch{return null}return await new Promise(f=>{l.toBlob(j=>f(j?URL.createObjectURL(j):null),"image/jpeg",.92)})},[]),[Ge,ys]=i.useState(null),[L,ka]=i.useState(null),Ke=i.useCallback((a,l)=>{const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;ea(f=>[...f,{id:c,text:a,variant:l}]),window.setTimeout(()=>{ea(f=>f.filter(j=>j.id!==c))},4800)},[]);i.useEffect(()=>{ta.current=k},[k]),i.useEffect(()=>{aa.current=T},[T]),i.useEffect(()=>{Ot.current=fe},[fe]),i.useEffect(()=>{Ft.current=Re},[Re]),i.useEffect(()=>{ct&&(Ke(ct,"error"),w(null))},[ct,Ke]),i.useEffect(()=>{_t&&(Ke(_t,"warn"),Ce(null))},[_t,Ke]);const qe=i.useCallback(()=>{const a=ta.current,l=aa.current;if(!a&&!l)return;const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;Qt(f=>[{id:c,at:Date.now(),single:a??void 0,batch:l??void 0},...f].slice(0,es))},[]),Je=()=>{ie.current!=null&&(window.clearTimeout(ie.current),ie.current=null)},Ze=()=>{le.current!=null&&(window.clearTimeout(le.current),le.current=null)},Pt=(L==null?void 0:L.star_analysis_min_interval_ms)??2e3,$t=(L==null?void 0:L.star_analysis_max_interval_ms)??12e3,Tt=i.useMemo(()=>{const a=(L==null?void 0:L.star_analysis_target_fps)??.6666666666666666,l=Math.min(Math.max(a,.2),5);return Math.round(1e3/l)},[L]),oe=i.useMemo(()=>Math.min(Math.max(kt??Tt,Pt),$t),[Tt,kt,$t,Pt]),Q=i.useCallback(async()=>{const[a,l,c]=await Promise.all([Js(),Ht("official"),Ht("user")]);R(a.files),M(l.presets),Yt(c.presets)},[]),At=i.useCallback(()=>{an().then(a=>ia(a.files)).catch(()=>ia([]))},[]);i.useEffect(()=>{ln().then(a=>{ka(a);const l=Math.round(1e3/Math.min(Math.max(a.star_analysis_target_fps,.2),5)),c=Math.min(Math.max(l,a.star_analysis_min_interval_ms),a.star_analysis_max_interval_ms);ga(c);const f=a.centroid_rejection_level_default;typeof f=="number"&&f>=1&&f<=5&&x(j=>({...j,centroid_rejection_level:f}))}).catch(()=>ka(null))},[]),i.useEffect(()=>{Q().catch(a=>w(String(a)))},[Q]),i.useEffect(()=>{At()},[At]),i.useEffect(()=>{n==="history"&&Wt(Ne,Ue,lt).then(dt).catch(a=>w(String(a)))},[n,Ne,Ue]),i.useEffect(()=>{if(!d){Ve(null);return}let a=!1;return ba(!0),(async()=>{try{const l=await nn(d);if(a)return;let c={...l};try{c={...await Ka(d),...l}}catch{}Ve(c)}catch{try{const l=await Ka(d);a||Ve(l)}catch{a||Ve(null)}}finally{a||ba(!1)}})(),()=>{a=!0}},[d]),i.useEffect(()=>{we(null),je(null),wt({}),jt(!1),J(null),Me(null),Se("file"),pt(null),Qt([])},[d]);const Z=i.useMemo(()=>{const a=k==null?void 0:k.result;if(!a)return null;const l=a.solve_overlay||null;if(!l)return null;const c=a.overlay_ext||null;return{...l,overlay_ext:c||void 0}},[k]),ee=i.useMemo(()=>(k==null?void 0:k.result)??null,[k]),Sa=i.useMemo(()=>Ln(ee),[ee]),Ut=i.useMemo(()=>n==="lab_image"?da:n==="lab_video"?S==="file"?pe:ua:{w:0,h:0},[n,S,da,pe,ua]),Ye=d?Vt(d):"";i.useEffect(()=>{if(n!=="lab_image")return;const a=Ct.current,l=he.current;if(!a||!l||!Z||!d)return;const c=()=>Ja(l,a,Z,V);a.complete?c():a.onload=c},[Z,d,k,V,n]),i.useEffect(()=>{if(n!=="lab_video"||S!=="file")return;const a=Ie.current,l=he.current;if(!a||!l||!Z||!d)return;const c=()=>mn(l,a,Z,V);return a.addEventListener("loadeddata",c),a.addEventListener("seeked",c),a.readyState>=2&&c(),()=>{a.removeEventListener("loadeddata",c),a.removeEventListener("seeked",c)}},[Z,V,n,S,d,k]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera")return;const a=Le.current,l=he.current;if(!a||!l||!Z)return;const c=()=>Ja(l,a,Z,V);a.complete?c():a.onload=c},[Z,V,n,S,k,xa,K]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera"){G("unknown");return}if(K){G("ok");return}let a=!1;return G("unknown"),(async()=>{try{const l=await as();if(a)return;if(l==="fail"){G("fail");return}if(l==="busy"){G("busy");return}G("ok"),It.current=0,We(Date.now())}catch{a||G("fail")}})(),()=>{a=!0}},[n,S,K]),i.useEffect(()=>{const a=Ct.current;if(!a)return;const l=()=>us({w:a.naturalWidth||0,h:a.naturalHeight||0});return l(),a.addEventListener("load",l),()=>a.removeEventListener("load",l)},[d,Ye]),i.useEffect(()=>{const a=Ie.current;if(!a)return;const l=()=>ms({w:a.videoWidth||0,h:a.videoHeight||0});return a.addEventListener("loadedmetadata",l),a.addEventListener("loadeddata",l),l(),()=>{a.removeEventListener("loadedmetadata",l),a.removeEventListener("loadeddata",l)}},[d,Ye,n]);const ws=a=>{x({...Jt(),...a,centroid:{...Jt().centroid,...a.centroid}})},js=async()=>{if(!d){w(e("err.selectFile"));return}w(null),U(!0);const a=performance.now();try{const l=await Xs(d,b);qe(),we(l),je(null),Me("file"),J(performance.now()-a)}catch(l){w(String(l)),J(null)}finally{U(!1)}},Ns=async()=>{if(!d){w(e("err.selectFile"));return}const a=[];for(const c of Xt){const j=[...I,...se].find($=>$.id===c);j&&a.push({label:j.name,params:structuredClone(j.params)})}if(a.length===0){w(e("err.selectPresets"));return}w(null),U(!0);const l=performance.now();try{const c=await Qs(d,a);qe(),je({results:c.results}),we(null),wt({}),Me("file"),J(performance.now()-l)}catch(c){w(String(c)),J(null)}finally{U(!1)}},Xe=async a=>{if(Lt.current)return;w(null),Lt.current=!0;const l=performance.now();try{const c=await on({source:"camera",overlay_topn_count:3,enable_polar_guide:!0,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),...b}),f=c.gate_status,j=c.next_allowed_in_ms,$=c.effective_interval_ms;if(f==="SKIPPED_INTERVAL"||f==="SKIPPED_BUSY"){if(Ce(ts(e,f,c.gate_reason,typeof j=="number"?j:null)),a){Ze();const C=Math.max(50,Number(j??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}return}Ce(null),c.result!=null&&(qe(),we(c)),je(null),Me("camera"),Se("camera");const E=c.result,D=typeof(E==null?void 0:E.status)=="string"?String(E.status):"";if(ha&&D==="MATCH_FOUND"){ft(!0),ht(c.frame_id!=null?String(c.frame_id):null);const C=await gs();vt(ce=>(ce&&URL.revokeObjectURL(ce),C)),tt()}else if(a){Ze();const C=Math.max(50,Number(j??$??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}J(performance.now()-l)}catch(c){w(String(c)),J(null)}finally{Lt.current=!1}},_s=()=>void Xe(!1),Qe=async a=>{if(Dt.current||!d)return;const l=Ie.current;if(!l||l.videoWidth<2||l.videoHeight<2||Ee)return;w(null),Dt.current=!0;const c=performance.now();try{const f=document.createElement("canvas");f.width=l.videoWidth,f.height=l.videoHeight;const j=f.getContext("2d");if(!j)throw new Error("无法创建画布上下文 / Cannot create canvas context");j.drawImage(l,0,0,f.width,f.height);const $=await new Promise((ve,Ts)=>{f.toBlob(Aa=>Aa?ve(Aa):Ts(new Error("帧编码失败 / Frame encode failed")),"image/jpeg",.92)}),E=await cn($,{...b,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),overlay_topn_count:3,enable_polar_guide:!0}),D=E.gate_status,C=E.next_allowed_in_ms,ce=E.effective_interval_ms;if(D==="SKIPPED_INTERVAL"||D==="SKIPPED_BUSY"){if(Ce(ts(e,D,E.gate_reason,typeof C=="number"?C:null)),a){Je();const ve=Math.max(50,Number(C??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}return}if(Ce(null),E.result!=null&&(qe(),we(E)),je(null),Me("file"),J(performance.now()-c),a){Je();const ve=Math.max(50,Number(C??ce??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}}catch(f){w(String(f)),J(null)}finally{Dt.current=!1}},ks=()=>void Qe(!1),Ss=()=>{fe||!d||(fa(!0),Je(),Qe(!0))},Ea=i.useMemo(()=>n!=="lab_video"||S!=="file"||!d||/\.avi$/i.test(d)||Ee?!1:pe.w>1&&pe.h>1,[n,S,d,Ee,pe.w,pe.h]),Ra=i.useMemo(()=>!!d&&/\.avi$/i.test(d),[d]),Es=async()=>{if(!(!d||!Ra||ya)){w(null),wa(!0),St(0),X(e("lab.transcode.loading"));try{const a=Vt(d),l=await fetch(a,{cache:"no-store"});if(!l.ok)throw new Error(e("lab.transcode.fetchFailed"));const c=await l.blob(),f=new File([c],d,{type:"video/x-msvideo"}),j=await Xa(f,(E,D)=>{St(Math.max(0,Math.min(1,E))),D==="loading_ffmpeg"?X(e("lab.transcode.loading")):D==="writing_input"?X(e("lab.transcode.writingBuffer")):D==="transcoding"?X(e("lab.transcode.running")):D==="packing_output"&&X(e("lab.transcode.packaging"))});X(e("lab.transcode.uploading"));const $=await Bt(j.file,"analysis_transcoded");await Ga({old_filename:d,new_filename:$.filename,duration_s:j.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),await Q(),m($.filename),St(1),X(e("lab.transcode.done"))}catch(a){w(String(a)),X(e("lab.transcode.failed"))}finally{wa(!1)}}},Rs=async a=>{H(e("sidebar.flowDownloadingPool")),re(.19);const l=Vt(a),c=await fetch(l,{cache:"no-store"});if(!c.ok)throw new Error(e("lab.transcode.fetchFailed"));const f=await c.blob(),j=new File([f],a,{type:"video/x-msvideo"}),$=await Xa(j,(D,C)=>{re(.2+Math.max(0,Math.min(1,D))*.6),C==="loading_ffmpeg"?H(e("sidebar.flowLoadingTranscoder")):C==="writing_input"?H(e("sidebar.flowWritingBuffer")):C==="transcoding"?H(e("sidebar.flowTranscoding")):C==="packing_output"&&H(e("sidebar.flowPackaging"))});H(e("sidebar.flowUploadingMp4")),re(.86);const E=await Bt($.file,"debug_console_transcoded");return H(e("sidebar.flowReplacing")),re(.94),await Ga({old_filename:a,new_filename:E.filename,duration_s:$.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),E.filename},Ms=async()=>{if(!B||Na)return;const a=/\.avi$/i.test(B);w(null),_a(!0),re(.02),Mt(null);try{H(e("sidebar.flowImportingDebug"));const l=await Ys(B);re(a?.18:1);let c=l.filename;a?c=await Rs(l.filename):H(e("sidebar.flowNoTranscode")),await Q(),m(c),r(ge(c)?"lab_video":"lab_image"),re(1),H(e("sidebar.flowDone")),Mt(e("sidebar.flowDoneMsg",{name:rt(c)}))}catch(l){w(String(l)),H(e("sidebar.flowFailed")),Mt(String(l))}finally{_a(!1)}},et=()=>{fa(!1),Je()},Cs=()=>{Re||K||(pa(!0),Ze(),Xe(!0))},tt=()=>{pa(!1),Ze()},Is=async()=>{try{const a=Le.current;a&&(a.onload=null,a.onerror=null,a.src="",a.removeAttribute("src")),await new Promise(l=>window.requestAnimationFrame(()=>l())),await fetch("/api/dev/debug/camera/stop",{method:"POST"}),We(Date.now()),S==="camera"&&Se("file")}catch{}},Ls=()=>{ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null)),We(Date.now())},Ds=a=>{is(l=>{const c=new Set(l);return c.has(a)?c.delete(a):c.add(a),c})},Ma=async a=>{if(!window.confirm(e("delete.uploadFirst",{name:a})))return;let l=0;try{l=(await rn(a)).count}catch{l=0}if(l>0){if(!window.confirm(e("delete.uploadCascade",{n:l})))return}else if(!window.confirm(e("delete.uploadSecond")))return;U(!0),w(null);try{await Zs(a,{deleteExperiments:l>0}),await Q(),d===a&&m(null)}catch(c){w(String(c))}finally{U(!1)}},Os=async a=>{if(window.confirm(e("delete.experimentFirst"))&&window.confirm(e("delete.experimentSecond"))){U(!0),w(null);try{await tn(a),na===a&&ra(null);const l=await Wt(Ne,Ue,lt);dt(l)}catch(l){w(String(l))}finally{U(!1)}}},at=a=>mt(Math.min(4,Math.max(.5,a))),Fs=Math.max(1,Math.ceil(((Be==null?void 0:Be.total)??0)/lt)),be=i.useMemo(()=>n==="lab_image"?He.filter(a=>a.type==="image"||Qa(a.name)):n==="lab_video"?He.filter(a=>a.type==="video"||ge(a.name)):He,[He,n]),De=Math.max(1,Math.ceil(be.length/qt)),Ps=i.useMemo(()=>{const a=(_e-1)*qt;return be.slice(a,a+qt)},[be,_e]);i.useEffect(()=>{ke(1)},[n]),i.useEffect(()=>{ke(a=>Math.min(a,De))},[De]),i.useEffect(()=>{B&&!be.some(a=>a.name===B)&&oa(null)},[be,B]);const Ca=i.useMemo(()=>Sn(gt,s),[gt,s]),$s=i.useMemo(()=>n==="lab_image"?y.filter(a=>Qa(a.filename)):n==="lab_video"?y.filter(a=>ge(a.filename)):y,[y,n]),v=i.useMemo(()=>rs(ee),[ee]),Ia=i.useMemo(()=>ls(v.prob),[v.prob]);return i.useEffect(()=>{jt(!1)},[k]),i.useEffect(()=>{if(n!=="lab_video")return;let a;const l=()=>{un().then(ys).catch(()=>{})};return l(),a=setInterval(l,1500),()=>{a&&clearInterval(a)}},[n]),i.useEffect(()=>{(n!=="lab_video"||S!=="camera")&&(tt(),ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null))),(n!=="lab_video"||S!=="file")&&et()},[n,S]),i.useEffect(()=>{d||et()},[d]),i.useEffect(()=>()=>{tt(),et()},[]),t.jsxs("div",{className:"flex h-dvh min-h-0 flex-col overflow-hidden bg-surface text-on-surface",children:[t.jsx(Mn,{view:n,setView:r}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex shrink-0 items-center justify-between gap-2 border-b border-outline-variant/15 bg-surface-container-low px-3 py-2 text-xs 2xl:hidden",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{g(!1),u(!0)},children:s==="zh"?"素材":"Assets"}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{u(!1),g(!0)},children:s==="zh"?"解算参数":"Solve parameters"})]}),(o||p)&&t.jsx("button",{type:"button",className:"fixed inset-x-0 bottom-0 top-[88px] z-40 bg-black/60 2xl:hidden","aria-label":s==="zh"?"关闭面板":"Close panel",onClick:()=>{u(!1),g(!1)}}),t.jsxs("div",{className:"flex min-h-0 flex-1",children:[t.jsxs("aside",{className:`fixed bottom-0 left-0 top-[88px] z-50 w-64 shrink-0 overflow-y-auto border-r border-outline-variant/15 bg-surface-container-lowest p-3 text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${o?"translate-x-0":"-translate-x-full"}`,children:[t.jsx("div",{className:"mb-2 flex justify-end 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>u(!1),children:s==="zh"?"关闭":"Close"})}),t.jsx("div",{className:"mb-3 font-semibold text-on-surface-variant",children:e("sidebar.assets")}),t.jsxs("label",{className:"mb-3 flex cursor-pointer items-center gap-2 rounded bg-primary-container/30 px-2 py-2 text-on-primary-container",children:[t.jsx(qs,{className:"h-4 w-4"}),t.jsx("span",{children:e("sidebar.upload")}),t.jsx("input",{type:"file",accept:"image/*,video/*",className:"hidden",onChange:async a=>{var c;const l=(c=a.target.files)==null?void 0:c[0];if(l){U(!0),w(null);try{const f=await Bt(l);await Q(),m(f.filename)}catch(f){w(String(f))}finally{U(!1),a.target.value=""}}}})]}),t.jsxs("button",{type:"button",className:"mb-2 flex w-full items-center gap-1 text-left text-on-surface-variant hover:text-on-surface",onClick:()=>{Q().catch(a=>w(String(a))),ke(1),At()},children:[t.jsx(Ws,{className:"h-3 w-3"})," ",e("sidebar.refresh")]}),t.jsx("div",{className:"og-scrollbar max-h-36 overflow-y-auto border-t border-outline-variant/10 pt-2",children:$s.map(a=>t.jsxs("div",{className:`mb-1 flex items-center gap-0.5 rounded px-1 ${d===a.filename?"bg-surface-container":""}`,children:[t.jsxs("button",{type:"button",className:`min-w-0 flex-1 truncate rounded px-1 py-1 text-left ${d===a.filename?"text-primary":""}`,title:a.filename,onClick:()=>{m(a.filename),r(ge(a.filename)?"lab_video":"lab_image")},children:[t.jsx("span",{className:"block truncate",children:rt(a.filename)}),t.jsx("span",{className:"block text-[9px] text-on-surface-variant/90",children:ge(a.filename)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")})]}),t.jsx("button",{type:"button",className:"shrink-0 rounded p-1 text-on-surface-variant hover:bg-surface-container-high hover:text-error",title:e("pool.delete"),"aria-label":e("pool.delete"),onClick:l=>{l.stopPropagation(),Ma(a.filename)},children:t.jsx(ss,{className:"h-3.5 w-3.5"})})]},a.filename))}),t.jsxs("div",{className:"mt-3 border-t border-outline-variant/10 pt-3",children:[t.jsxs("div",{className:"mb-2 flex items-start justify-between gap-2",children:[t.jsx("div",{className:"min-w-0 font-semibold leading-tight text-on-surface-variant",children:e("sidebar.debugCaptures")}),t.jsx("button",{type:"button",className:"shrink-0 rounded bg-primary px-2 py-1 text-[10px] font-medium text-on-primary disabled:opacity-40",disabled:!B||Na,title:B??void 0,onClick:()=>void Ms(),children:B&&/\.avi$/i.test(B)?e("sidebar.importToPoolWithTranscode"):e("sidebar.importToPoolDirect")})]}),Et!=null&&t.jsxs("div",{className:"mb-2 rounded border border-outline-variant/25 bg-surface-container-low px-2 py-2",children:[t.jsxs("div",{className:"flex items-center justify-between gap-2 text-[10px]",children:[t.jsx("span",{className:"text-on-surface-variant",children:vs||e("sidebar.flowPreparing")}),t.jsxs("span",{className:"font-mono text-on-surface",children:[Math.round((Et||0)*100),"%"]})]}),t.jsx("div",{className:"mt-1.5 h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round((Et||0)*100)}%`}})}),Rt&&t.jsx("div",{className:"mt-1 text-[10px] text-on-surface-variant",title:Rt,children:Rt})]}),be.length===0?t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("sidebar.debugEmpty")}):t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"og-scrollbar max-h-[min(22rem,55vh)] space-y-1.5 overflow-y-auto pr-0.5",children:Ps.map(a=>t.jsxs("button",{type:"button",className:`flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left transition-colors ${B===a.name?"border-primary bg-primary-container/20":"border-outline-variant/25 hover:bg-surface-container"}`,onClick:()=>oa(a.name),children:[a.type==="image"?t.jsx("img",{src:sn(a.name),alt:"",className:"h-11 w-11 shrink-0 rounded object-cover"}):t.jsx("div",{className:"flex h-11 w-11 shrink-0 items-center justify-center rounded bg-surface-container-high text-[9px] text-on-surface-variant",children:"video"}),t.jsxs("div",{className:"min-w-0 flex-1",children:[t.jsx("div",{className:"truncate font-mono text-[10px] text-on-surface",title:a.name,children:rt(a.name)}),t.jsxs("div",{className:"text-[9px] text-on-surface-variant",children:[a.type==="video"||ge(a.name)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")," ","· ",$e(a.modified,s)," · ",Zt(a.size)]})]})]},a.name))}),t.jsxs("div",{className:"mt-2 flex items-center justify-between gap-1 text-[10px] text-on-surface-variant",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e<=1,onClick:()=>ke(a=>Math.max(1,a-1)),children:e("history.prev")}),t.jsx("span",{className:"tabular-nums",children:e("sidebar.debugPage",{cur:_e,total:De})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e>=De,onClick:()=>ke(a=>Math.min(De,a+1)),children:e("history.next")})]})]})]})]}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1",children:[t.jsxs("main",{className:"og-scrollbar min-h-0 min-w-0 flex-1 overflow-y-auto p-4",children:[n==="lab_video"&&t.jsxs("div",{className:"mb-3 max-w-5xl rounded-lg border border-outline-variant/25 bg-surface-container-low/80 p-3 text-[11px] leading-relaxed text-on-surface",children:[t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.videoLiveIntro")}),t.jsx("p",{className:"mt-1.5 text-[10px] leading-snug text-on-surface-variant/95",children:e("lab.videoMjpegHint")}),t.jsxs("div",{className:"mt-2 flex flex-wrap gap-2",children:[t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="file"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("file"),children:e("lab.previewModeFile")}),t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="camera"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("camera"),children:e("lab.previewModeCamera")}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-[11px] font-medium text-on-surface",onClick:()=>void Is(),children:e("lab.stopCameraPreview")})]})]}),t.jsxs("div",{className:"relative aspect-video w-full max-w-5xl overflow-hidden rounded-lg border border-outline-variant/20 bg-surface-container-lowest",children:[Ia!=null&&t.jsxs("div",{className:"pointer-events-none absolute right-2 top-2 z-20 rounded border border-white/30 bg-black/55 px-2 py-1 text-[10px] font-semibold tabular-nums text-white",children:[e("lab.previewConfidence"),": ",Ia.toFixed(1),"%"]}),n==="lab_video"&&S==="camera"?t.jsx("div",{className:"relative flex h-full min-h-[220px] flex-col items-center justify-center gap-3 bg-black p-2",children:t.jsxs("div",{className:"relative inline-block max-h-[70vh] max-w-full",children:[K&&bt?t.jsx("img",{ref:Le,src:bt,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight})}):K&&!bt?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):xt==="busy"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.videoMjpegHint")})}):xt==="fail"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.cameraStreamStatusFail")})}):xt==="unknown"?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):t.jsx("img",{ref:Le,src:`/api/dev/debug/camera/stream?t=${xa}`,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight}),onError:()=>{(async()=>{const a=await as();if(a==="busy"){G("busy");return}if(a==="fail"){G("fail");return}if(It.current>=6){G("fail");return}It.current+=1,window.setTimeout(()=>We(Date.now()),400)})()}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}):d?t.jsx("div",{className:"og-scrollbar relative h-full min-h-[200px] overflow-auto",children:n==="lab_video"?t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(Ha,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(za,{className:"inline h-3 w-3"})})]}),t.jsxs("div",{className:"flex min-h-[200px] flex-col items-center justify-center gap-2 bg-black py-2",children:[t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("video",{ref:Ie,src:Ye,loop:!0,playsInline:!0,autoPlay:!0,muted:!0,preload:"metadata",className:"max-h-[70vh] w-full max-w-full object-contain",onError:()=>pt(e("lab.videoPreviewFailed")),onLoadedData:()=>{pt(null);const a=Ie.current;a&&a.play().catch(()=>{})}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}),Ee&&t.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-1.5 text-[11px] text-error",children:Ee}),Ra&&t.jsxs("div",{className:"max-w-3xl rounded border border-primary/40 bg-primary/10 px-3 py-2 text-[11px] text-on-surface",children:[t.jsx("div",{className:"font-semibold text-primary",children:e("lab.transcode.title")}),t.jsx("div",{className:"mt-1 text-on-surface-variant",children:e("lab.transcode.desc")}),ja!=null&&t.jsxs("div",{className:"mt-2",children:[t.jsx("div",{className:"mb-1 text-[10px] text-on-surface-variant",children:bs||e("lab.transcode.running")}),t.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round(ja*100)}%`}})})]}),t.jsx("div",{className:"mt-2",children:t.jsx("button",{type:"button",className:"rounded bg-primary px-3 py-1.5 text-[11px] font-medium text-on-primary disabled:opacity-50",disabled:ya,onClick:()=>void Es(),children:e("lab.transcode.button")})})]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(Ha,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(za,{className:"inline h-3 w-3"})})]}),t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("img",{ref:Ct,src:Ye,alt:"preview",className:"max-h-[70vh] w-full object-contain"}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})})]})}):t.jsx("div",{className:"flex h-full items-center justify-center px-4 text-center text-on-surface-variant",children:e(n==="lab_video"?"lab.selectOrUploadVideo":"lab.selectOrUpload")}),ne&&t.jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/40",children:t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"})})]}),(d&&n==="lab_image"||n==="lab_video"&&(S==="file"&&d||S==="camera"))&&t.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 px-3 py-2 text-xs text-on-surface",children:[t.jsx("span",{className:"shrink-0 font-medium text-on-surface-variant",children:e("lab.layers")}),t.jsx("div",{className:"flex flex-wrap gap-x-4 gap-y-1",children:["matched","pattern","all","rejected"].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-1",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:V[a],onChange:l=>ds(c=>({...c,[a]:l.target.checked}))}),t.jsx("span",{children:e(a==="matched"?"lab.layer.matched":a==="pattern"?"lab.layer.pattern":a==="all"?"lab.layer.all":"lab.layer.rejected")})]},a))})]}),(d||n==="lab_video"&&S==="camera")&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 grid gap-2 sm:grid-cols-2",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.solveSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:ee?t.jsxs("div",{className:"space-y-1 text-on-surface",children:[v.tSolveMs!=null&&t.jsxs("div",{className:"space-y-0.5",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveComputeMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[v.tSolveMs.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveComputeHelp")})]}),Nt!=null&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveRoundTripMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[Nt.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveRoundTripHelp")})]}),(v.tBackendTotalMs!=null||v.tOpenDecodeMs!=null||v.tPreprocessMs!=null||v.tExtractMs!=null||v.tSolveMs!=null)&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.backendTotalMs")}),t.jsx("span",{className:"font-mono tabular-nums",children:v.tBackendTotalMs!=null?`${v.tBackendTotalMs.toFixed(0)} ms`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[8px] text-on-surface-variant/90",children:[t.jsxs("span",{children:[e("lab.metric.openDecodeMs"),":"," ",v.tOpenDecodeMs!=null?`${v.tOpenDecodeMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.preprocessMs"),":"," ",v.tPreprocessMs!=null?`${v.tPreprocessMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.extractMs"),":"," ",v.tExtractMs!=null?`${v.tExtractMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.solveOnlyMs"),":"," ",v.tSolveMs!=null?`${v.tSolveMs.toFixed(0)} ms`:e("common.placeholder")]})]})]}),(v.raDeg!=null||v.decDeg!=null)&&t.jsxs("div",{className:"leading-tight",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.radec")}),t.jsxs("div",{className:"mt-0.5 font-mono text-[9px]",children:["α ",it(v.raDeg)," · δ"," ",it(v.decDeg)]})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[9px]",children:[v.matches!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.matches")})," ",t.jsx("span",{className:"font-mono",children:v.matches})]}),v.rmseArcsec!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.rmse")})," ",t.jsxs("span",{className:"font-mono",children:[v.rmseArcsec.toFixed(2),"″"]})]}),v.prob!=null&&t.jsxs("span",{className:"inline-flex max-w-full flex-col gap-0.5",children:[t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.prob")})," ",t.jsx("span",{className:"font-mono",children:ye(v.prob,ee??void 0).line})]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probHelp")}),ye(v.prob,ee??void 0).rawLine&&t.jsxs(t.Fragment,{children:[t.jsxs("span",{className:"text-[8px] text-on-surface-variant",children:[e("lab.metric.probRaw"),":"," ",ye(v.prob,ee??void 0).rawLine]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probRawHelp")})]})]})]}),v.status&&t.jsxs("div",{className:"border-t border-outline-variant/15 pt-1 text-[9px]",children:[t.jsxs("span",{className:"text-on-surface-variant",children:[e("lab.metric.status")," "]}),t.jsx("span",{className:"font-mono text-secondary",children:v.status})]})]}):t.jsx("p",{className:"text-on-surface-variant",children:e("common.placeholder")})})]}),t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.imageSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.resolution")}),t.jsx("span",{className:"font-mono tabular-nums",children:Ut.w>0?`${Ut.w}×${Ut.h}`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.starsDetected")}),t.jsx("span",{className:"font-mono tabular-nums",children:Sa??e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.fwhm")}),t.jsx("span",{children:e("common.placeholder")})]})]})]})]}),d&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 flex flex-wrap gap-4 text-xs text-on-surface-variant",children:[t.jsxs("span",{children:[e("lab.file"),":"," ",t.jsx("span",{className:"font-mono text-on-surface",title:d,children:rt(d,22,18)})]}),((La=y.find(a=>a.filename===d))==null?void 0:La.source)&&t.jsxs("span",{className:"rounded bg-surface-container-high px-2 py-0.5",children:[e("lab.source"),":"," ",(Da=y.find(a=>a.filename===d))==null?void 0:Da.source]})]}),t.jsxs("details",{open:!0,className:"mt-2 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 text-xs shadow-sm",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center gap-2 px-3 py-2 font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.meta.title"),yt&&t.jsx(st,{className:"h-3.5 w-3.5 animate-spin"}),t.jsx(Fe,{className:"ml-auto h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:Ca.length>0?t.jsx("dl",{className:"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-3",children:Ca.map(a=>t.jsxs("div",{children:[t.jsx("dt",{className:"text-[10px] text-on-surface-variant",children:e(a.key)}),t.jsx("dd",{className:"text-[11px] font-medium text-on-surface",children:a.value})]},`${a.key}-${a.value}`))}):gt&&!yt?t.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:e("lab.meta.partial")}):yt?null:t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.meta.noSidecar")})})]})]})]}),(d||n==="lab_video"&&S==="camera")&&(Ae.length>0||k||T)&&t.jsxs(t.Fragment,{children:[Ae.length>0&&t.jsxs("section",{className:"mt-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.solveHistoryTitle")}),t.jsxs("span",{className:"ml-2 normal-case text-on-surface-variant/70",children:["(",Ae.length,"/",es,")"]})]}),t.jsx("div",{className:"og-scrollbar max-h-[min(36vh,18rem)] space-y-2 overflow-y-auto p-3",children:Ae.map(a=>{var $;const l=a.batch,c=a.single,f=$e(new Date(a.at).toISOString(),s),j=l&&l.results.length?e("results.historyBatch",{count:l.results.length}):e("results.historySingle");return t.jsxs("details",{className:"rounded border border-outline-variant/20 bg-surface-container/80 text-xs",children:[t.jsx("summary",{className:"cursor-pointer list-none px-3 py-2 font-medium text-on-surface [&::-webkit-details-marker]:hidden",children:t.jsxs("span",{children:[f," · ",j]})}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[($=l==null?void 0:l.results)!=null&&$.length?t.jsx("div",{className:"og-scrollbar flex gap-2 overflow-x-auto pb-1",children:l.results.map((E,D)=>{const C=E.result,ce=E.success===!0;return t.jsxs("div",{className:"min-w-[12rem] max-w-xs shrink-0 rounded border border-outline-variant/20 bg-surface-container-lowest p-2",children:[t.jsx("div",{className:"border-b border-outline-variant/10 pb-1 text-[10px] font-semibold text-secondary",children:String(E.label??D)}),ce&&C?t.jsx(nt,{result:C,t:e,roundTripMs:null}):t.jsx("div",{className:"mt-1 text-[10px] text-error",children:String(E.error??"—")})]},D)})}):null,c&&c.result?t.jsx("div",{className:"mt-2 rounded border border-outline-variant/15 p-2",children:t.jsx(nt,{result:c.result,t:e,roundTripMs:null})}):null,t.jsxs("details",{className:"mt-2",children:[t.jsx("summary",{className:"cursor-pointer text-[10px] text-primary hover:underline",children:e("results.viewRaw")}),t.jsx("pre",{className:"og-scrollbar mt-1 max-h-32 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify({single:c,batch:l},null,2)})]})]})]},a.id)})})]}),(k||T)&&t.jsxs("section",{className:"mt-3 max-h-[min(50vh,28rem)] rounded-lg border border-outline-variant/25 bg-surface-container-lowest/95",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center justify-between border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.title")}),t.jsxs("div",{className:"flex gap-3",children:[(Oa=T==null?void 0:T.results)!=null&&Oa.length?t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!(!d||!T)){for(const a of T.results){if(!a.success)continue;const l=a.result;await zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(l??null),replay:{layers:V,params:b}})}w(null)}},children:e("results.saveBatchAll")}):null,k&&t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!d&&hs!=="camera")return;const a=k.result;await zt({input_name:d??e("lab.cameraSnapshotName"),preset_label:"manual",result_json:k,metrics:Kt(a??null),replay:{layers:V,params:b}}),w(null)},children:e("results.saveCurrent")})]})]}),t.jsx("div",{className:"og-scrollbar min-h-0 overflow-y-auto p-3",children:t.jsxs("div",{className:"og-scrollbar flex gap-3 overflow-x-auto text-xs",children:[T==null?void 0:T.results.map((a,l)=>{const c=a.result,f=fs[l]??!1;return t.jsxs("div",{className:"min-w-[15rem] max-w-sm shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 font-semibold text-secondary",children:String(a.label)}),a.success?t.jsxs(t.Fragment,{children:[t.jsx(nt,{result:c,t:e,roundTripMs:null}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>wt(j=>({...j,[l]:!f})),children:e(f?"results.hideRaw":"results.viewRaw")}),f&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-40 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(a,null,2)})]}):t.jsx("div",{className:"mt-2 text-[10px] text-error",children:String(a.error)}),a.success===!0&&d?t.jsx("button",{type:"button",className:"mt-3 w-full rounded bg-surface-container-high py-1.5 text-[10px] font-medium",onClick:()=>zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(c??null),replay:{layers:V,params:b}}).catch(j=>w(String(j))),children:e("results.saveRow")}):null]},l)}),k&&!T&&t.jsxs("div",{className:"min-w-[16rem] max-w-md shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 text-[10px] font-semibold uppercase text-on-surface-variant",children:e("results.title")}),t.jsx(nt,{result:k.result,t:e,roundTripMs:Nt}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>jt(a=>!a),children:e(va?"results.hideRaw":"results.viewRaw")}),va&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-48 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(k,null,2)})]})]})})]})]})]}),t.jsxs("aside",{className:`fixed bottom-0 right-0 top-[88px] z-50 flex min-h-0 w-80 shrink-0 flex-col border-l border-outline-variant/15 bg-surface-container-low text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${p?"translate-x-0":"translate-x-full"}`,children:[t.jsx("div",{className:"flex shrink-0 justify-end border-b border-outline-variant/15 p-2 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>g(!1),children:s==="zh"?"关闭":"Close"})}),n!=="lab_video"&&t.jsxs("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container",onClick:js,disabled:ne,children:e("btn.solveOne")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary",onClick:Ns,disabled:ne,children:e("btn.solveBatch")})]}),n==="lab_video"&&t.jsx("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:S==="file"?t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Ss(),disabled:ne||!Ea||fe,children:e("lab.solveFileStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>et(),disabled:!fe,children:e("lab.solveFileStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void ks(),disabled:ne||!Ea||fe,children:e("lab.solveCurrentFrame")})]}):t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Cs(),disabled:ne||Re||K,children:e("lab.solveCameraStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>tt(),disabled:!Re,children:e("lab.solveCameraStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void _s(),disabled:ne||K,children:e("lab.solveCameraFrame")}),t.jsxs("label",{className:"flex items-center gap-2 rounded border border-outline-variant/25 px-2 py-1.5 text-[11px]",children:[t.jsx("input",{type:"checkbox",checked:ha,onChange:a=>xs(a.target.checked)}),t.jsx("span",{children:"自动保持(解算成功后冻结) / Auto Hold"})]}),K&&t.jsx("button",{type:"button",className:"w-full rounded bg-tertiary/80 py-2.5 font-semibold text-on-tertiary disabled:opacity-40",onClick:()=>Ls(),children:"继续实时 / Resume Live"}),K&&t.jsxs("div",{className:"text-[10px] text-on-surface-variant",children:["已冻结帧 / Frozen frame ",ps??"-"]})]})}),n==="lab_video"&&Ge&&t.jsxs("div",{className:"shrink-0 border-b border-outline-variant/20 px-4 py-2 text-[10px] text-on-surface-variant",children:[t.jsx("div",{className:"font-medium text-on-surface",children:e("lab.systemLoad")}),t.jsxs("div",{children:["CPU ",Ge.cpu_usage,"% · RAM ",Ge.memory_usage,"% ·"," ",Ge.temperature,"°C"]})]}),t.jsxs("div",{className:"og-scrollbar min-h-0 flex-1 overflow-y-auto p-4",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.title"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockSolveIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.solveProfile")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.solveProfileHelp")}),t.jsxs("select",{className:"w-full rounded bg-surface-container-highest px-2 py-1",value:b.solve_profile??"balanced",onChange:a=>x(l=>({...l,solve_profile:a.target.value})),children:[t.jsx("option",{value:"speed",children:e("params.solveProfileSpeed")}),t.jsx("option",{value:"balanced",children:e("params.solveProfileBalanced")}),t.jsx("option",{value:"robust",children:e("params.solveProfileRobust")})]})]}),t.jsx(z,{label:e("params.fov"),helpKey:"params.fovHelp",value:b.fov_estimate??"",onChange:a=>x(l=>({...l,fov_estimate:a}))}),t.jsx(z,{label:e("params.fovErr"),helpKey:"params.fovErrHelp",value:b.fov_max_error??"",onChange:a=>x(l=>({...l,fov_max_error:a}))}),t.jsx(z,{label:e("params.timeout"),helpKey:"params.timeoutHelp",value:b.solve_timeout_ms??"",onChange:a=>x(l=>({...l,solve_timeout_ms:a}))}),t.jsx(z,{label:e("params.solveIntervalMs"),helpKey:"params.solveIntervalMsHelp",value:kt??"",onChange:a=>ga(a==null?Tt:Math.round(a))}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalBound",{min:Pt,max:$t,effective:oe})}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalIndependent")}),n==="lab_video"&&t.jsxs("label",{className:"mt-2 block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.centroidRejectionLevel")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.centroidRejectionLevelHelp")}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"1"}),t.jsx("input",{type:"range",min:1,max:5,step:1,className:"h-2 flex-1 accent-primary",value:b.centroid_rejection_level??3,onChange:a=>x(l=>({...l,centroid_rejection_level:Number(a.target.value)}))}),t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"5"}),t.jsx("span",{className:"ml-1 min-w-[1.25rem] tabular-nums text-[10px] font-semibold text-on-surface",children:b.centroid_rejection_level??3})]}),t.jsx("p",{className:"mt-0.5 text-[9px] text-on-surface-variant/90",children:e("params.centroidRejectionScale")})]}),t.jsx(z,{label:e("params.ra"),helpKey:"params.raHelp",value:b.hint_ra_deg??"",onChange:a=>x(l=>({...l,hint_ra_deg:a}))}),t.jsx(z,{label:e("params.dec"),helpKey:"params.decHelp",value:b.hint_dec_deg??"",onChange:a=>x(l=>({...l,hint_dec_deg:a}))}),t.jsx(z,{label:e("params.maxSide"),helpKey:"params.maxSideHelp",value:b.max_image_side??"",onChange:a=>x(l=>({...l,max_image_side:a}))}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:b.detail_level==="full",onChange:a=>x(l=>({...l,detail_level:a.target.checked?"full":"summary"}))}),t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.detailLevelFull")})]}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-start gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary mt-0.5",checked:!!b.large_scale_bg_subtract,onChange:a=>x(l=>({...l,large_scale_bg_subtract:a.target.checked}))}),t.jsxs("span",{children:[t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.largeScaleBg")}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.largeScaleBgHelp")})]})]})]})]})]}),t.jsxs("details",{open:!0,className:"mt-3 rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.centroid"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockCentroidIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsx(z,{label:e("params.sigma"),helpKey:"params.sigmaHelp",step:.1,value:((Fa=b.centroid)==null?void 0:Fa.sigma)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,sigma:a}}))}),t.jsx(z,{label:e("params.maxArea"),helpKey:"params.maxAreaHelp",value:((Pa=b.centroid)==null?void 0:Pa.max_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,max_area:a}}))}),t.jsx(z,{label:e("params.minArea"),helpKey:"params.minAreaHelp",value:(($a=b.centroid)==null?void 0:$a.min_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,min_area:a}}))}),t.jsx(z,{label:e("params.filtsize"),helpKey:"params.filtsizeHelp",step:2,value:((Ta=b.centroid)==null?void 0:Ta.filtsize)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,filtsize:a}}))})]})]})]}),t.jsxs("div",{className:"mt-4 rounded-md border border-outline-variant/20 bg-surface-container-highest/40 p-3",children:[t.jsx("div",{className:"mb-2 font-semibold text-on-surface-variant",children:e("sidebar.batchPresets")}),t.jsx("p",{className:"mb-2 text-[10px] leading-snug text-on-surface-variant",children:e("sidebar.batchHint")}),t.jsx("div",{className:"og-scrollbar max-h-36 space-y-1.5 overflow-y-auto",children:[...I,...se].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",checked:Xt.has(a.id),onChange:()=>Ds(a.id)}),t.jsx("span",{className:"truncate",children:a.name})]},a.id))})]}),t.jsxs("div",{className:"mt-6 border-t border-outline-variant/20 pt-4",children:[t.jsx("div",{className:"mb-2 font-semibold",children:e("btn.applyPresets")}),t.jsx("div",{className:"og-scrollbar max-h-24 space-y-1 overflow-y-auto",children:[...I,...se].map(a=>t.jsx("button",{type:"button",className:"block w-full truncate text-left text-primary hover:underline",onClick:()=>ws(a.params),children:a.name},a.id))}),t.jsxs("div",{className:"mt-3 flex gap-1",children:[t.jsx("input",{className:"flex-1 rounded bg-surface-container-highest px-2 py-1",placeholder:e("placeholder.newPreset"),value:ut,onChange:a=>la(a.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container-high px-2",onClick:async()=>{if(ut.trim()){U(!0);try{await en(ut.trim(),b),la(""),await Q();const a=await Ht("user");Yt(a.presets)}catch(a){w(String(a))}finally{U(!1)}}},children:e("btn.savePreset")})]})]})]})]})]}),n==="pool"&&t.jsx(In,{uploads:y,onDelete:a=>void Ma(a)}),n==="history"&&t.jsx(Cn,{historyQ:Ne,setHistoryQ:cs,historyPage:Ue,setHistoryPage:sa,historyData:Be,historyExpandId:na,setHistoryExpandId:ra,historyTotalPages:Fs,onDeleteExperiment:a=>void Os(a),onSearch:()=>Wt(Ne,1,lt).then(a=>{sa(1),dt(a)})})]}),t.jsx("div",{className:"pointer-events-none fixed bottom-4 right-4 z-[100] flex max-w-sm flex-col gap-2",children:os.map(a=>t.jsx("div",{className:`pointer-events-auto rounded border px-3 py-2 text-left text-xs shadow-lg ${a.variant==="error"?"border-error/50 bg-error-container/95 text-on-error-container":a.variant==="warn"?"border-amber-500/40 bg-surface-container-highest/95 text-on-surface":"border-outline-variant/40 bg-surface-container-high/95 text-on-surface"}`,children:a.text},a.id))})]})}Bs.createRoot(document.getElementById("root")).render(t.jsx(Hs.StrictMode,{children:t.jsx(zs,{children:t.jsx(Dn,{})})})); diff --git a/web/static/analysis-lab/assets/camera-aU1xxh-S.js b/web/static/analysis-lab/assets/camera-aU1xxh-S.js new file mode 100644 index 0000000..f557e53 --- /dev/null +++ b/web/static/analysis-lab/assets/camera-aU1xxh-S.js @@ -0,0 +1,56 @@ +import{r as s,j as e,a as Ft,R as $t}from"./client-D1ZVDB-N.js";import{c as _,u as Bt,T as Pt,I as Tt}from"./index-CV2LaaT8.js";import{u as It,S as Ot,C as Ua,X as Dt,r as At,b as Ht}from"./http-lbh6zBy9.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 La=_("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** + * @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 zt=_("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @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 Gt=_("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** + * @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 qt=_("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** + * @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 Ut=_("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** + * @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 Lt=_("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @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 Wa=_("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @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 Wt=_("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** + * @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 Vt=_("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** + * @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 Jt=_("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** + * @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 Kt=_("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Zt="/api/dev/debug";function L(t){return`${Zt}${t}`}async function d(t,g={}){const v=t.startsWith("/api/debug/")?t.replace("/api/debug/","/api/dev/debug/"):t;return At(v,g)}const Xt=["640x360","1280x720","1600x900","1920x1020"],Qt=[0,90,180,270],ke=12;function w(t,g,v){return Math.min(v,Math.max(g,t))}function j(t,g){return t==null||Number.isNaN(Number(t))?g:Number(t)}function Va(t){if(!t)return"0 B";const g=["B","KB","MB","GB"];let v=0,h=t;for(;h>=1024&&v0&&h>=v?"busy":"ok"}catch{return"fail"}}function $({label:t,value:g,min:v,max:h,step:n,onChange:_e,disabled:u=!1,unit:W=""}){return e.jsxs("label",{className:`block ${u?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:t}),e.jsxs("span",{className:"font-mono text-[11px]",children:[g.toFixed(n>=1?0:n>=.1?1:2),W]})]}),e.jsx("input",{type:"range",min:v,max:h,step:n,value:g,disabled:u,onChange:te=>_e(Number(te.target.value)),className:"w-full accent-primary"})]})}function Yt(){var Ra,Ea,Ma,Fa,$a,Ba,Pa,Ta,Ia,Oa,Da,Aa,Ha;const{t,locale:g,setLocale:v}=Bt(),{info:h}=It(),[n,_e]=s.useState(null),[u,W]=s.useState(!1),[te,Ce]=s.useState(()=>Date.now()),[Re,i]=s.useState(null),[re,H]=s.useState(null),[We,z]=s.useState(!1),[V,p]=s.useState(null),[M,ne]=s.useState(!1),[Ee,Ve]=s.useState(!1),[Je,Ke]=s.useState(!1),[Ze,Xe]=s.useState("8"),[Qe,Ye]=s.useState("8"),[ea,aa]=s.useState("1280x720"),[ta,ra]=s.useState("supersample"),[J,G]=s.useState(!1),[na,Ka]=s.useState(!0),[Me,Za]=s.useState(!0),[Fe,Xa]=s.useState(!1),[sa,Qa]=s.useState(!1),[oa,Ya]=s.useState(!1),[$e,et]=s.useState({mean:0,std:0,over:0}),[at,se]=s.useState(0),[o,Be]=s.useState(null),[ia,ca]=s.useState(0),[la,da]=s.useState(180),[oe,ua]=s.useState(!1),[ie,ma]=s.useState(!1),[c,y]=s.useState({exposure:5e3,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxUs:2e6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[ce,b]=s.useState(!1),[Pe,pa]=s.useState(""),[xa,ha]=s.useState(""),[fa,tt]=s.useState([]),[Te,B]=s.useState(!1),[K,rt]=s.useState([]),[ba,ga]=s.useState(!1),[N,Ie]=s.useState(null),[nt,Oe]=s.useState(!1),[le,De]=s.useState(null),[Ae,de]=s.useState(1),C=s.useRef(null),ue=s.useRef(null),Z=s.useRef(null),me=s.useRef(null),pe=s.useRef(null),P=s.useRef(null),xe=s.useRef(null),D=s.useRef(!1),he=s.useRef(null),T=s.useRef([]),q=s.useRef(0),He=s.useRef(null),va=()=>{T.current=[],q.current=0,he.current=null,se(0)},S=async()=>{try{const a=await d("/api/debug/camera/status",{cache:"no-store"});_e(a),a.streaming||(W(!1),D.current=!1)}catch(a){i(a instanceof Error?a.message:String(a))}},X=()=>{xe.current!=null&&(window.clearTimeout(xe.current),xe.current=null)},ya=async()=>{if(!M){ne(!0),i(null),H(null),z(!1);try{X(),n!=null&&n.streaming||await d("/api/debug/camera/start",{method:"POST"});const a=Date.now(),r=await Ja();if(r==="busy"){H(t("cam.err.streamBusy")),z(!0);return}if(r==="fail"){H(t("cam.err.streamProbeFailed")),z(!1);return}W(!0),D.current=!0,p(t("cam.notice.previewStart")),va(),he.current=performance.now(),Ce(a),await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},st=async()=>{if(!M){ne(!0),i(null);try{X(),W(!1),D.current=!1,H(null),z(!1),va(),C.current&&(C.current.onload=null,C.current.onerror=null,C.current.src="",C.current.removeAttribute("src")),Ce(Date.now()),p(t("cam.notice.previewStop"))}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},ot=async()=>{if(!(!u&&!(n!=null&&n.streaming))){Ke(!0),i(null);try{const a=await d("/api/debug/camera/capture",{method:"POST"});p(t("cam.notice.captureSaved",{name:a.filename||"capture"}))}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ke(!1)}}},it=async()=>{if(!Ee){Ve(!0),i(null);try{if(n!=null&&n.recording)await d("/api/debug/camera/record/stop",{method:"POST"}),p(t("cam.notice.recordStop"));else{const a=await d("/api/debug/camera/record/start",{method:"POST"});p(t("cam.notice.recordStart",{name:a.filename||"video.avi"}))}await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ve(!1)}}},ct=async()=>{i(null);try{const a=w(parseInt(Ze,10)||5,1,60);await d(`/api/debug/camera/fps?fps=${a}`,{method:"POST"});const r=w(parseInt(Qe,10)||8,1,30);await d(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[m,x]=ea.split("x").map(l=>parseInt(l,10));m&&x&&await d(`/api/debug/camera/size?width=${m}&height=${x}`,{method:"POST"}),await d(`/api/debug/camera/sampling?mode=${encodeURIComponent(ta)}`,{method:"POST"}),p(t("cam.notice.runtimeApplied")),G(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},lt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.settingsApplied")),b(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},dt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.modeApplied")),b(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},ut=(a,r)=>{if(!a)return;const m=(r==null?void 0:r.syncRuntime)??!0;y({exposure:w(Math.round(j(a.exposure_us,5e3)),100,12e4),gain:w(j(a.analogue_gain,1),1,24),digitalGain:w(j(a.digital_gain,1),1,8),autoExposure:!!(a.auto_exposure??!0),contrast:w(j(a.contrast,1),0,2),brightness:w(j(a.brightness,0),-1,1),saturation:w(j(a.saturation,1),0,2),sharpness:w(j(a.sharpness,1),0,2),noiseReduction:w(Math.round(j(a.noise_reduction,0)),0,4),noiseReductionMode:String(a.noise_reduction_mode??"fast"),aeFlickerMode:String(a.ae_flicker_mode??"off"),autoExposureMaxUs:w(Math.round(j(a.auto_exposure_max_us,2e6)),1e4,1e7),whiteBalanceMode:String(a.white_balance_mode??"auto"),whiteBalanceGainR:w(j(a.white_balance_gain_r,1),.1,3),whiteBalanceGainB:w(j(a.white_balance_gain_b,1),.1,3),colorMode:String(a.color_mode??"color")}),m&&(Xe(String(Math.round(j(a.fps,8)))),aa(`${Math.round(j(a.width,1280))}x${Math.round(j(a.height,720))}`),ra(String(a.sampling_mode??"supersample")),G(!1)),da(w(Math.round(j(a.rotation,180)),0,270)),ua(!!a.flip_horizontal),ma(!!a.flip_vertical),b(!1)},wa=async(a,r)=>{i(null);try{await d("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:a,flip_vertical:r})}),ua(a),ma(r),p(t("cam.notice.mirrorApplied")),await S()}catch(m){i(m instanceof Error?m.message:String(m))}},mt=async a=>{i(null);try{await d(`/api/debug/camera/rotation/${a}`,{method:"POST"}),da(a),p(t("cam.notice.rotationApplied",{value:a})),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},ja=async a=>{i(null);try{await d(`/api/debug/camera/night-mode?enabled=${a?"true":"false"}`,{method:"POST"}),p(t(a?"cam.notice.nightOn":"cam.notice.nightOff")),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},pt=async()=>{if(window.confirm(t("cam.confirm.reset"))){i(null);try{await d("/api/debug/camera/reset",{method:"POST"}),p(t("cam.notice.reset")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}}},xt=async()=>{i(null);try{await d("/api/debug/camera/backup-settings",{method:"POST"}),p(t("cam.notice.backup"))}catch(a){i(a instanceof Error?a.message:String(a))}},ht=async()=>{i(null);try{await d("/api/debug/camera/restore-settings",{method:"POST"}),p(t("cam.notice.restore")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},fe=async()=>{B(!0);try{const a=await d("/api/debug/camera/presets",{cache:"no-store"});tt(a.presets??[])}catch(a){i(a instanceof Error?a.message:String(a))}finally{B(!1)}},ft=async()=>{const a=Pe.trim();if(a){B(!0),i(null);try{await d("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:a,description:xa.trim(),exposure_us:c.exposure,analogue_gain:c.gain,digital_gain:c.digitalGain,auto_exposure:c.autoExposure,contrast:c.contrast,brightness:c.brightness,saturation:c.saturation,sharpness:c.sharpness,noise_reduction:c.noiseReduction,white_balance_mode:c.whiteBalanceMode,white_balance_gain_r:c.whiteBalanceGainR,white_balance_gain_b:c.whiteBalanceGainB,rotation:la,flip_horizontal:oe,flip_vertical:ie,color_mode:c.colorMode})}),p(t("cam.notice.presetSaved",{name:a})),pa(""),ha(""),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},bt=async a=>{B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}/apply`,{method:"POST"}),p(t("cam.notice.presetApplied",{name:a})),await S(),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}},gt=async a=>{if(window.confirm(t("cam.confirm.deletePreset",{name:a}))){B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.presetDeleted",{name:a})),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},ze=async()=>{ga(!0);try{const a=await d("/api/debug/files",{cache:"no-store"});rt(a.files??[]),de(1)}catch(a){i(a instanceof Error?a.message:String(a))}finally{ga(!1)}},Ge=()=>{De(null),Ie(null),Oe(!1)},vt=async a=>{if(le===a){Ge();return}De(a),Ie(null),Oe(!0),i(null);try{const r=await d(`/api/debug/files/${encodeURIComponent(a)}/info`,{cache:"no-store"});Ie(r)}catch(r){i(r instanceof Error?r.message:String(r)),De(null)}finally{Oe(!1)}},yt=a=>{const r=(x,l)=>{const f=document.createElement("a");f.href=l,f.download=x,document.body.appendChild(f),f.click(),document.body.removeChild(f)};r(a,`${L("/files")}/${encodeURIComponent(a)}`);const m=a.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(m){const l=`${a.slice(0,-m[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${L("/files")}/${encodeURIComponent(l)}`)).ok)return;r(l,`${L("/files")}/${encodeURIComponent(l)}`),p(t("cam.notice.downloadWithSidecar",{name:a,sidecar:l}))}catch{p(t("cam.notice.download",{name:a}))}})();return}p(t("cam.notice.download",{name:a}))},wt=async a=>{if(window.confirm(t("cam.confirm.deleteFile",{name:a}))){i(null);try{await d(`/api/debug/files/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.fileDeleted",{name:a})),(le===a||(N==null?void 0:N.filename)===a)&&Ge(),await ze()}catch(r){i(r instanceof Error?r.message:String(r))}}},jt=()=>{if(!na||!C.current||!ue.current)return;const a=C.current;if(!a.naturalWidth||!a.naturalHeight||(Z.current||(Z.current=document.createElement("canvas"),me.current=Z.current.getContext("2d",{willReadFrequently:!0})),pe.current||(pe.current=ue.current.getContext("2d")),!me.current||!pe.current))return;const m=Math.min(1,320/a.naturalWidth),x=Math.max(1,Math.round(a.naturalWidth*m)),l=Math.max(1,Math.round(a.naturalHeight*m));Z.current.width=x,Z.current.height=l,me.current.drawImage(a,0,0,x,l);const f=me.current.getImageData(0,0,x,l).data,F=new Array(256).fill(0),k=new Array(256).fill(0),Q=new Array(256).fill(0),Ue=new Array(256).fill(0);let za=0,Ga=0,qa=0;const U=x*l;for(let E=0;E=250&&(qa+=1)}const Le=U?za/U:0,Et=U?Ga/U-Le*Le:0;et({mean:Le,std:Math.sqrt(Math.max(0,Et)),over:U?qa/U*100:0});const we=ue.current,R=pe.current,je=window.devicePixelRatio||1,Y=Math.max(1,we.clientWidth||240),ee=Math.max(1,we.clientHeight||120);we.width=Math.floor(Y*je),we.height=Math.floor(ee*je),R.setTransform(je,0,0,je,0,0),R.clearRect(0,0,Y,ee);const Mt=Math.max(1,...Me?[Math.max(...F),Math.max(...k),Math.max(...Q)]:[0],...Fe?[Math.max(...Ue)]:[0]),Ne=(E,Se)=>{R.beginPath(),R.strokeStyle=Se,R.lineWidth=1.2;for(let I=0;I<256;I+=1){const ae=I/255*Y,O=ee-E[I]/Mt*ee;I===0?R.moveTo(ae,O):R.lineTo(ae,O)}R.stroke()};if(Me&&(Ne(F,"rgba(255,80,80,0.85)"),Ne(k,"rgba(80,255,80,0.85)"),Ne(Q,"rgba(80,160,255,0.85)")),Fe&&Ne(Ue,"rgba(255,255,255,0.95)"),sa){const E=.9803921568627451*Y;R.fillStyle="rgba(255,100,100,0.12)",R.fillRect(E,0,Y-E,ee)}};s.useEffect(()=>{S(),fe(),ze();const a=window.setInterval(()=>{document.hidden||S()},2e3);return()=>window.clearInterval(a)},[n==null?void 0:n.streaming]),s.useEffect(()=>{!(n!=null&&n.info)||ce||ut(n.info,{syncRuntime:!J})},[n==null?void 0:n.info,ce,J]),s.useEffect(()=>{if(!(n!=null&&n.recording)){P.current&&(window.clearInterval(P.current),P.current=null),ca(0);return}if(P.current)return;const a=Date.now();return P.current=window.setInterval(()=>{ca(Math.max(0,Math.floor((Date.now()-a)/1e3)))},1e3),()=>{P.current&&(window.clearInterval(P.current),P.current=null)}},[n==null?void 0:n.recording]),s.useEffect(()=>{D.current=u,u||X()},[u]),s.useEffect(()=>{u&&(T.current=[],q.current=0)},[te,u]),s.useEffect(()=>{if(!u){T.current=[],q.current=0,se(0);return}if(!He.current){const l=document.createElement("canvas");l.width=32,l.height=32,He.current=l}const r=He.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let m=0;const x=()=>{const l=C.current;if(l!=null&&l.complete&&l.naturalWidth>0)try{r.drawImage(l,0,0,32,32);const f=r.getImageData(0,0,32,32).data;let F=2166136261;for(let k=0;kwindow.cancelAnimationFrame(m)},[u]),s.useEffect(()=>{if(!u){se(0);return}const a=window.setInterval(()=>{const m=performance.now()-1e3,x=T.current;for(;x.length&&x[0]window.clearInterval(a)},[u]),s.useEffect(()=>{if(!u){Be(null);return}let a=!1;const r=async()=>{try{const x=await fetch(`${L("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!x.ok||a)return;const l=await x.json();if(!a){Be(l);const f=Number(l.preview_target_fps??l.target_preview_fps??0);Number.isFinite(f)&&f>0&&!J&&Ye(String(Math.round(f)))}}catch{a||Be(null)}};r();const m=window.setInterval(r,2e3);return()=>{a=!0,window.clearInterval(m)}},[u,J]),s.useEffect(()=>{if(!V)return;const a=window.setTimeout(()=>p(null),3200);return()=>window.clearTimeout(a)},[V]),s.useEffect(()=>()=>X(),[]),s.useEffect(()=>{const a=Math.max(1,Math.ceil(K.length/ke));Ae>a&&de(a)},[K.length,Ae]);const Nt=u?`${L("/camera/stream")}?t=${te}`:"",be=c.autoExposure,qe=c.whiteBalanceMode==="manual",A=((Ra=n==null?void 0:n.info)==null?void 0:Ra.capabilities)??{},St=Array.isArray(A.awb_modes)&&A.awb_modes.length>0?A.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],kt=Array.isArray(A.noise_reduction_modes)&&A.noise_reduction_modes.length>0?A.noise_reduction_modes:["off","fast","high_quality"],Na=A.manual_digital_gain!==!1,Sa=!!((Ea=n==null?void 0:n.info)!=null&&Ea.night_mode),ka=u,ge=!M&&!u&&!(n!=null&&n.recording),_a=!M&&u,_t=!M&&!Je&&ka,Ct=!M&&!Ee&&ka,ve=Math.max(1,Math.ceil(K.length/ke)),ye=Math.min(Ae,ve),Ca=(ye-1)*ke,Rt=K.slice(Ca,Ca+ke);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${t("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("zh"),children:t("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("en"),children:t("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Wt,{className:"h-3.5 w-3.5"})," ",t("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Qt.map(a=>e.jsxs("button",{type:"button",onClick:()=>void mt(a),className:`rounded border px-2 py-1 ${la===a?"border-primary text-primary":"border-outline-variant/30"}`,children:[a,"°"]},a))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void wa(!oe,ie),className:`rounded border px-2 py-1 ${oe?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void wa(oe,!ie),className:`rounded border px-2 py-1 ${ie?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void xt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ot,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ht(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(qt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:t("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:t("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ja(!0),className:`rounded border px-2 py-1 ${Sa?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Lt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ja(!1),className:`rounded border px-2 py-1 ${Sa?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Kt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:Pe,onChange:a=>pa(a.target.value),placeholder:t("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:xa,onChange:a=>ha(a.target.value),placeholder:t("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:Te||!Pe.trim(),onClick:()=>void ft(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[fa.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.presets.empty")}),fa.map(a=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:a.name}),e.jsx("div",{className:"text-on-surface-variant",children:a.description||t("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[t("cam.controls.exposure"),": ",a.exposure_us,"us | ",t("cam.controls.gain"),": ",a.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:Te,onClick:()=>void bt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:Te,onClick:()=>void gt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.delete")})]})]},a.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.cpu_usage)==null?"—":`${Number(h.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.memory_usage)==null?"—":`${Number(h.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.temperature)==null?"—":`${Number(h.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:t("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"shrink-0 font-mono text-xs text-on-surface-variant",children:[t("cam.preview.state"),": ",t(u?"cam.state.streaming":"cam.state.idle")]})]}),re&&!u&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:"relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black",children:[u&&re&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),u?e.jsx("img",{ref:C,alt:"camera-preview",className:"h-full w-full object-contain",src:Nt,onLoad:()=>{H(null),z(!1),jt()},onError:()=>{var r;if(X(),!D.current)return;const a=(r=C.current)==null?void 0:r.src;(async()=>{if(!D.current||!a)return;if(await Ja()==="busy"){H(t("cam.err.streamBusy")),z(!0);return}xe.current=window.setTimeout(()=>{D.current&&Ce(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ua,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:t("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:t("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"mr-1 inline h-3.5 w-3.5"}),t(M?"cam.btn.starting":"cam.btn.start")]})]}),(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(La,{className:"h-3.5 w-3.5 fill-current"})," ",t("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(ia/60).toString().padStart(2,"0")}:${(ia%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Ya(a=>!a),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:t(oa?"cam.hist.expand":"cam.hist.collapse")})}),!oa&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:na,onChange:a=>Ka(a.target.checked)})," ",t("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Me,onChange:a=>Za(a.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Fe,onChange:a=>Xa(a.target.checked)})," ",t("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:sa,onChange:a=>Qa(a.target.checked)})," ",t("cam.hist.over")]})]}),e.jsx("canvas",{ref:ue,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:$e.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:$e.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[$e.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_preview_fps)??at).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((Ma=n==null?void 0:n.info)==null?void 0:Ma.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(o==null?void 0:o.preview_target_fps)??(o==null?void 0:o.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((o==null?void 0:o.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((o==null?void 0:o.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(o==null?void 0:o.jpeg_source_format)??"RGB888"} / fail ${(o==null?void 0:o.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(o==null?void 0:o.preview_consumers)??0}/${(o==null?void 0:o.analysis_consumers)??0}/${(o==null?void 0:o.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((o==null?void 0:o.process_rss_kb)??0)/1024)} / ${Math.round(Number((o==null?void 0:o.process_swap_kb)??0)/1024)} MB`})]}),(o==null?void 0:o.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:t("cam.stats.longExposureThrottle",{exposure:Math.round(Number(o.actual_exposure_us??0)/1e3)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:he.current!=null?`${Math.max(0,Math.round((performance.now()-he.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((Fa=n==null?void 0:n.info)==null?void 0:Fa.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String((($a=n==null?void 0:n.info)==null?void 0:$a.driver)??(o==null?void 0:o.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Ba=n==null?void 0:n.info)==null?void 0:Ba.backend)??(o==null?void 0:o.camera_backend)??"—"} · lores ${(Pa=n==null?void 0:n.info)!=null&&Pa.lores_available||o!=null&&o.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((Ta=n==null?void 0:n.info)==null?void 0:Ta.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((Ia=n==null?void 0:n.info)==null?void 0:Ia.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Oa=n==null?void 0:n.info)==null?void 0:Oa.width)??"—"}x${((Da=n==null?void 0:n.info)==null?void 0:Da.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Aa=n==null?void 0:n.info)!=null&&Aa.auto_exposure?t("cam.controls.auto"):t("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!_a,onClick:()=>void st(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${_a?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Jt,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!_t,onClick:()=>void ot(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ua,{className:"inline h-4 w-4"})," ",t(Je?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!Ct,onClick:()=>void it(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(La,{className:"inline h-4 w-4"})," ",Ee?t("cam.btn.recordBusy"):n!=null&&n.recording?t("cam.btn.recordStop"):t("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:ba,onClick:()=>void ze(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.refresh")})}),nt&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:t("cam.files.loadingInfo")}),N&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Gt,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:N.filename})]}),e.jsx("button",{type:"button",onClick:()=>Ge(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":t("cam.files.closeDetail"),children:e.jsx(Dt,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[t("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Va(N.size)})]}),e.jsxs("div",{children:[t("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:N.type})]}),e.jsxs("div",{children:[t("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(N.modified).toLocaleString()})]}),N.exposure_us!=null&&e.jsxs("div",{children:[t("cam.controls.exposure"),": ",e.jsxs("span",{className:"font-mono",children:[N.exposure_us,"us"]})]}),N.analogue_gain!=null&&e.jsxs("div",{children:[t("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:N.analogue_gain})]}),N.resolution&&e.jsxs("div",{children:[t("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:N.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[K.length===0&&!ba&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.empty")}),Rt.map(a=>e.jsxs("div",{className:`rounded border p-2 ${le===a.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:a.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Va(a.size)," | ",new Date(a.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:a.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>yt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(zt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void vt(a.name),className:`rounded border px-2 py-1 ${le===a.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Ut,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void wt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Pt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.delete")]})]})]},a.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ye<=1,onClick:()=>de(a=>Math.max(1,a-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.page",{current:ye,total:ve})}),e.jsx("button",{type:"button",disabled:ye>=ve,onClick:()=>de(a=>Math.min(ve,a+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.sensorFps"),e.jsx("input",{value:Ze,onChange:a=>{Xe(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.previewFps"),e.jsx("input",{value:Qe,onChange:a=>{Ye(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[t("cam.controls.sampling"),e.jsxs("select",{value:ta,onChange:a=>{ra(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[t("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Xt.map(a=>e.jsx("button",{type:"button",onClick:()=>{aa(a),G(!0)},className:`rounded border px-2 py-1 ${ea===a?"border-primary text-primary":"border-outline-variant/30"}`,children:a},a))})]}),e.jsx("button",{type:"button",disabled:!J,onClick:()=>void ct(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Vt,{className:"h-3.5 w-3.5"})," ",t("cam.controls.core")]}),ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:t("cam.controls.pendingChanges")}),be&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx($,{label:t("cam.controls.exposure"),value:c.exposure,min:100,max:12e4,step:100,unit:"us",disabled:be,onChange:a=>{b(!0),y(r=>({...r,exposure:a}))}}),e.jsx($,{label:t("cam.controls.gain"),value:c.gain,min:1,max:24,step:.1,disabled:be,onChange:a=>{b(!0),y(r=>({...r,gain:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.digitalGain"),value:c.digitalGain,min:1,max:8,step:.1,disabled:be||!Na,onChange:a=>{b(!0),y(r=>({...r,digitalGain:Number(a.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[t("cam.controls.noiseReductionMode"),e.jsx("select",{value:c.noiseReductionMode,onChange:a=>{b(!0),y(r=>({...r,noiseReductionMode:a.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:kt.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.nr.${a}`)},a))})]}),e.jsx($,{label:t("cam.controls.contrast"),value:c.contrast,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,contrast:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.brightness"),value:c.brightness,min:-1,max:1,step:.1,onChange:a=>{b(!0),y(r=>({...r,brightness:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.saturation"),value:c.saturation,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,saturation:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.sharpness"),value:c.sharpness,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,sharpness:Number(a.toFixed(1))}))}})]}),!Na&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[t("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ha=n==null?void 0:n.info)==null?void 0:Ha.actual_digital_gain)??c.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!ce,onClick:()=>void lt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.autoExposure"),e.jsxs("select",{value:c.autoExposure?"auto":"manual",onChange:a=>{y(r=>({...r,autoExposure:a.target.value==="auto"})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:t("cam.controls.auto")}),e.jsx("option",{value:"manual",children:t("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.colorMode"),e.jsxs("select",{value:c.colorMode,onChange:a=>{y(r=>({...r,colorMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:t("cam.controls.color")}),e.jsx("option",{value:"mono",children:t("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.whiteBalance"),e.jsx("select",{value:c.whiteBalanceMode,onChange:a=>{y(r=>({...r,whiteBalanceMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:St.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.wb.${a}`)},a))})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.aeFlicker"),e.jsxs("select",{value:c.aeFlickerMode,onChange:a=>{y(r=>({...r,aeFlickerMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:t("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:1e4,max:1e7,step:1e4,value:c.autoExposureMaxUs,onChange:a=>{y(r=>({...r,autoExposureMaxUs:w(parseInt(a.target.value,10)||2e6,1e4,1e7)})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx($,{label:"R Gain",value:c.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainR:Number(a.toFixed(1))})),b(!0)}}),e.jsx($,{label:"B Gain",value:c.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainB:Number(a.toFixed(1))})),b(!0)}})]}),!qe&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void dt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:t("cam.controls.applyMode")})})]})]})]})]}),(Re||V)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[Re&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:Re}),V&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:V})]})]})}Ft.createRoot(document.getElementById("root")).render(e.jsx($t.StrictMode,{children:e.jsx(Tt,{children:e.jsx(Ht,{children:e.jsx(Yt,{})})})})); diff --git a/web/static/analysis-lab/assets/camera-hzdNY7z2.js b/web/static/analysis-lab/assets/camera-hzdNY7z2.js deleted file mode 100644 index 9d54da9..0000000 --- a/web/static/analysis-lab/assets/camera-hzdNY7z2.js +++ /dev/null @@ -1,61 +0,0 @@ -import{r as s,j as e,a as Ft,R as $t}from"./client-D1ZVDB-N.js";import{c as _,u as Bt,T as Pt,I as Tt}from"./index-C78KOEFu.js";import{u as It,S as Ot,C as Ua,r as Dt,b as At}from"./http-VZMNcsmS.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 La=_("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** - * @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 Ht=_("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** - * @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 zt=_("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** - * @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 Gt=_("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** - * @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 qt=_("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** - * @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 Ut=_("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** - * @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 Wa=_("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** - * @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 Lt=_("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** - * @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 Wt=_("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** - * @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 Vt=_("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** - * @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 Jt=_("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]);/** - * @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 Kt=_("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]),Xt="/api/dev/debug";function L(t){return`${Xt}${t}`}async function d(t,b={}){const v=t.startsWith("/api/debug/")?t.replace("/api/debug/","/api/dev/debug/"):t;return Dt(v,b)}const Zt=["640x360","1280x720","1600x900","1920x1020"],Qt=[0,90,180,270],ke=12;function j(t,b,v){return Math.min(v,Math.max(b,t))}function w(t,b){return t==null||Number.isNaN(Number(t))?b:Number(t)}function Va(t){if(!t)return"0 B";const b=["B","KB","MB","GB"];let v=0,g=t;for(;g>=1024&&v0&&g>=v?"busy":"ok"}catch{return"fail"}}function $({label:t,value:b,min:v,max:g,step:n,onChange:_e,disabled:u=!1,unit:W=""}){return e.jsxs("label",{className:`block ${u?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:t}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(n>=1?0:n>=.1?1:2),W]})]}),e.jsx("input",{type:"range",min:v,max:g,step:n,value:b,disabled:u,onChange:te=>_e(Number(te.target.value)),className:"w-full accent-primary"})]})}function Yt(){var Ra,Ea,Ma,Fa,$a,Ba,Pa,Ta,Ia,Oa,Da,Aa,Ha;const{t,locale:b,setLocale:v}=Bt(),{info:g}=It(),[n,_e]=s.useState(null),[u,W]=s.useState(!1),[te,Ce]=s.useState(()=>Date.now()),[Re,i]=s.useState(null),[re,H]=s.useState(null),[We,z]=s.useState(!1),[V,p]=s.useState(null),[M,ne]=s.useState(!1),[Ee,Ve]=s.useState(!1),[Je,Ke]=s.useState(!1),[Xe,Ze]=s.useState("8"),[Qe,Ye]=s.useState("8"),[ea,aa]=s.useState("1280x720"),[ta,ra]=s.useState("supersample"),[J,G]=s.useState(!1),[na,Ka]=s.useState(!0),[Me,Xa]=s.useState(!0),[Fe,Za]=s.useState(!1),[sa,Qa]=s.useState(!1),[oa,Ya]=s.useState(!1),[$e,et]=s.useState({mean:0,std:0,over:0}),[at,se]=s.useState(0),[o,Be]=s.useState(null),[ia,ca]=s.useState(0),[la,da]=s.useState(180),[oe,ua]=s.useState(!1),[ie,ma]=s.useState(!1),[c,y]=s.useState({exposure:5e3,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxUs:2e6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[ce,f]=s.useState(!1),[Pe,pa]=s.useState(""),[xa,ha]=s.useState(""),[fa,tt]=s.useState([]),[Te,B]=s.useState(!1),[K,rt]=s.useState([]),[ba,ga]=s.useState(!1),[N,Ie]=s.useState(null),[nt,Oe]=s.useState(!1),[le,De]=s.useState(null),[Ae,de]=s.useState(1),C=s.useRef(null),ue=s.useRef(null),X=s.useRef(null),me=s.useRef(null),pe=s.useRef(null),P=s.useRef(null),xe=s.useRef(null),D=s.useRef(!1),he=s.useRef(null),T=s.useRef([]),q=s.useRef(0),He=s.useRef(null),va=()=>{T.current=[],q.current=0,he.current=null,se(0)},S=async()=>{try{const a=await d("/api/debug/camera/status",{cache:"no-store"});_e(a),a.streaming||(W(!1),D.current=!1)}catch(a){i(a instanceof Error?a.message:String(a))}},Z=()=>{xe.current!=null&&(window.clearTimeout(xe.current),xe.current=null)},ya=async()=>{if(!M){ne(!0),i(null),H(null),z(!1);try{Z(),n!=null&&n.streaming||await d("/api/debug/camera/start",{method:"POST"});const a=Date.now(),r=await Ja();if(r==="busy"){H(t("cam.err.streamBusy")),z(!0);return}if(r==="fail"){H(t("cam.err.streamProbeFailed")),z(!1);return}W(!0),D.current=!0,p(t("cam.notice.previewStart")),va(),he.current=performance.now(),Ce(a),await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},st=async()=>{if(!M){ne(!0),i(null);try{Z(),W(!1),D.current=!1,H(null),z(!1),va(),C.current&&(C.current.onload=null,C.current.onerror=null,C.current.src="",C.current.removeAttribute("src")),Ce(Date.now()),p(t("cam.notice.previewStop"))}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},ot=async()=>{if(!(!u&&!(n!=null&&n.streaming))){Ke(!0),i(null);try{const a=await d("/api/debug/camera/capture",{method:"POST"});p(t("cam.notice.captureSaved",{name:a.filename||"capture"}))}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ke(!1)}}},it=async()=>{if(!Ee){Ve(!0),i(null);try{if(n!=null&&n.recording)await d("/api/debug/camera/record/stop",{method:"POST"}),p(t("cam.notice.recordStop"));else{const a=await d("/api/debug/camera/record/start",{method:"POST"});p(t("cam.notice.recordStart",{name:a.filename||"video.avi"}))}await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ve(!1)}}},ct=async()=>{i(null);try{const a=j(parseInt(Xe,10)||5,1,60);await d(`/api/debug/camera/fps?fps=${a}`,{method:"POST"});const r=j(parseInt(Qe,10)||8,1,30);await d(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[m,x]=ea.split("x").map(l=>parseInt(l,10));m&&x&&await d(`/api/debug/camera/size?width=${m}&height=${x}`,{method:"POST"}),await d(`/api/debug/camera/sampling?mode=${encodeURIComponent(ta)}`,{method:"POST"}),p(t("cam.notice.runtimeApplied")),G(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},lt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.settingsApplied")),f(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},dt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.modeApplied")),f(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},ut=(a,r)=>{if(!a)return;const m=(r==null?void 0:r.syncRuntime)??!0;y({exposure:j(Math.round(w(a.exposure_us,5e3)),100,12e4),gain:j(w(a.analogue_gain,1),1,24),digitalGain:j(w(a.digital_gain,1),1,8),autoExposure:!!(a.auto_exposure??!0),contrast:j(w(a.contrast,1),0,2),brightness:j(w(a.brightness,0),-1,1),saturation:j(w(a.saturation,1),0,2),sharpness:j(w(a.sharpness,1),0,2),noiseReduction:j(Math.round(w(a.noise_reduction,0)),0,4),noiseReductionMode:String(a.noise_reduction_mode??"fast"),aeFlickerMode:String(a.ae_flicker_mode??"off"),autoExposureMaxUs:j(Math.round(w(a.auto_exposure_max_us,2e6)),1e4,1e7),whiteBalanceMode:String(a.white_balance_mode??"auto"),whiteBalanceGainR:j(w(a.white_balance_gain_r,1),.1,3),whiteBalanceGainB:j(w(a.white_balance_gain_b,1),.1,3),colorMode:String(a.color_mode??"color")}),m&&(Ze(String(Math.round(w(a.fps,8)))),aa(`${Math.round(w(a.width,1280))}x${Math.round(w(a.height,720))}`),ra(String(a.sampling_mode??"supersample")),G(!1)),da(j(Math.round(w(a.rotation,180)),0,270)),ua(!!a.flip_horizontal),ma(!!a.flip_vertical),f(!1)},ja=async(a,r)=>{i(null);try{await d("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:a,flip_vertical:r})}),ua(a),ma(r),p(t("cam.notice.mirrorApplied")),await S()}catch(m){i(m instanceof Error?m.message:String(m))}},mt=async a=>{i(null);try{await d(`/api/debug/camera/rotation/${a}`,{method:"POST"}),da(a),p(t("cam.notice.rotationApplied",{value:a})),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},wa=async a=>{i(null);try{await d(`/api/debug/camera/night-mode?enabled=${a?"true":"false"}`,{method:"POST"}),p(t(a?"cam.notice.nightOn":"cam.notice.nightOff")),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},pt=async()=>{i(null);try{await d("/api/debug/camera/reset",{method:"POST"}),p(t("cam.notice.reset")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},xt=async()=>{i(null);try{await d("/api/debug/camera/backup-settings",{method:"POST"}),p(t("cam.notice.backup"))}catch(a){i(a instanceof Error?a.message:String(a))}},ht=async()=>{i(null);try{await d("/api/debug/camera/restore-settings",{method:"POST"}),p(t("cam.notice.restore")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},fe=async()=>{B(!0);try{const a=await d("/api/debug/camera/presets",{cache:"no-store"});tt(a.presets??[])}catch(a){i(a instanceof Error?a.message:String(a))}finally{B(!1)}},ft=async()=>{const a=Pe.trim();if(a){B(!0),i(null);try{await d("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:a,description:xa.trim(),exposure_us:c.exposure,analogue_gain:c.gain,digital_gain:c.digitalGain,auto_exposure:c.autoExposure,contrast:c.contrast,brightness:c.brightness,saturation:c.saturation,sharpness:c.sharpness,noise_reduction:c.noiseReduction,white_balance_mode:c.whiteBalanceMode,white_balance_gain_r:c.whiteBalanceGainR,white_balance_gain_b:c.whiteBalanceGainB,rotation:la,flip_horizontal:oe,flip_vertical:ie,color_mode:c.colorMode})}),p(t("cam.notice.presetSaved",{name:a})),pa(""),ha(""),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},bt=async a=>{B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}/apply`,{method:"POST"}),p(t("cam.notice.presetApplied",{name:a})),await S(),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}},gt=async a=>{if(window.confirm(t("cam.confirm.deletePreset",{name:a}))){B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.presetDeleted",{name:a})),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},ze=async()=>{ga(!0);try{const a=await d("/api/debug/files",{cache:"no-store"});rt(a.files??[]),de(1)}catch(a){i(a instanceof Error?a.message:String(a))}finally{ga(!1)}},Ge=()=>{De(null),Ie(null),Oe(!1)},vt=async a=>{if(le===a){Ge();return}De(a),Ie(null),Oe(!0),i(null);try{const r=await d(`/api/debug/files/${encodeURIComponent(a)}/info`,{cache:"no-store"});Ie(r)}catch(r){i(r instanceof Error?r.message:String(r)),De(null)}finally{Oe(!1)}},yt=a=>{const r=(x,l)=>{const h=document.createElement("a");h.href=l,h.download=x,document.body.appendChild(h),h.click(),document.body.removeChild(h)};r(a,`${L("/files")}/${encodeURIComponent(a)}`);const m=a.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(m){const l=`${a.slice(0,-m[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${L("/files")}/${encodeURIComponent(l)}`)).ok)return;r(l,`${L("/files")}/${encodeURIComponent(l)}`),p(t("cam.notice.downloadWithSidecar",{name:a,sidecar:l}))}catch{p(t("cam.notice.download",{name:a}))}})();return}p(t("cam.notice.download",{name:a}))},jt=async a=>{if(window.confirm(t("cam.confirm.deleteFile",{name:a}))){i(null);try{await d(`/api/debug/files/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.fileDeleted",{name:a})),(le===a||(N==null?void 0:N.filename)===a)&&Ge(),await ze()}catch(r){i(r instanceof Error?r.message:String(r))}}},wt=()=>{if(!na||!C.current||!ue.current)return;const a=C.current;if(!a.naturalWidth||!a.naturalHeight||(X.current||(X.current=document.createElement("canvas"),me.current=X.current.getContext("2d",{willReadFrequently:!0})),pe.current||(pe.current=ue.current.getContext("2d")),!me.current||!pe.current))return;const m=Math.min(1,320/a.naturalWidth),x=Math.max(1,Math.round(a.naturalWidth*m)),l=Math.max(1,Math.round(a.naturalHeight*m));X.current.width=x,X.current.height=l,me.current.drawImage(a,0,0,x,l);const h=me.current.getImageData(0,0,x,l).data,F=new Array(256).fill(0),k=new Array(256).fill(0),Q=new Array(256).fill(0),Ue=new Array(256).fill(0);let za=0,Ga=0,qa=0;const U=x*l;for(let E=0;E=250&&(qa+=1)}const Le=U?za/U:0,Et=U?Ga/U-Le*Le:0;et({mean:Le,std:Math.sqrt(Math.max(0,Et)),over:U?qa/U*100:0});const je=ue.current,R=pe.current,we=window.devicePixelRatio||1,Y=Math.max(1,je.clientWidth||240),ee=Math.max(1,je.clientHeight||120);je.width=Math.floor(Y*we),je.height=Math.floor(ee*we),R.setTransform(we,0,0,we,0,0),R.clearRect(0,0,Y,ee);const Mt=Math.max(1,...Me?[Math.max(...F),Math.max(...k),Math.max(...Q)]:[0],...Fe?[Math.max(...Ue)]:[0]),Ne=(E,Se)=>{R.beginPath(),R.strokeStyle=Se,R.lineWidth=1.2;for(let I=0;I<256;I+=1){const ae=I/255*Y,O=ee-E[I]/Mt*ee;I===0?R.moveTo(ae,O):R.lineTo(ae,O)}R.stroke()};if(Me&&(Ne(F,"rgba(255,80,80,0.85)"),Ne(k,"rgba(80,255,80,0.85)"),Ne(Q,"rgba(80,160,255,0.85)")),Fe&&Ne(Ue,"rgba(255,255,255,0.95)"),sa){const E=.9803921568627451*Y;R.fillStyle="rgba(255,100,100,0.12)",R.fillRect(E,0,Y-E,ee)}};s.useEffect(()=>{S(),fe(),ze();const a=window.setInterval(()=>{document.hidden||S()},2e3);return()=>window.clearInterval(a)},[n==null?void 0:n.streaming]),s.useEffect(()=>{!(n!=null&&n.info)||ce||ut(n.info,{syncRuntime:!J})},[n==null?void 0:n.info,ce,J]),s.useEffect(()=>{if(!(n!=null&&n.recording)){P.current&&(window.clearInterval(P.current),P.current=null),ca(0);return}if(P.current)return;const a=Date.now();return P.current=window.setInterval(()=>{ca(Math.max(0,Math.floor((Date.now()-a)/1e3)))},1e3),()=>{P.current&&(window.clearInterval(P.current),P.current=null)}},[n==null?void 0:n.recording]),s.useEffect(()=>{D.current=u,u||Z()},[u]),s.useEffect(()=>{u&&(T.current=[],q.current=0)},[te,u]),s.useEffect(()=>{if(!u){T.current=[],q.current=0,se(0);return}if(!He.current){const l=document.createElement("canvas");l.width=32,l.height=32,He.current=l}const r=He.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let m=0;const x=()=>{const l=C.current;if(l!=null&&l.complete&&l.naturalWidth>0)try{r.drawImage(l,0,0,32,32);const h=r.getImageData(0,0,32,32).data;let F=2166136261;for(let k=0;kwindow.cancelAnimationFrame(m)},[u]),s.useEffect(()=>{if(!u){se(0);return}const a=window.setInterval(()=>{const m=performance.now()-1e3,x=T.current;for(;x.length&&x[0]window.clearInterval(a)},[u]),s.useEffect(()=>{if(!u){Be(null);return}let a=!1;const r=async()=>{try{const x=await fetch(`${L("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!x.ok||a)return;const l=await x.json();if(!a){Be(l);const h=Number(l.preview_target_fps??l.target_preview_fps??0);Number.isFinite(h)&&h>0&&!J&&Ye(String(Math.round(h)))}}catch{a||Be(null)}};r();const m=window.setInterval(r,2e3);return()=>{a=!0,window.clearInterval(m)}},[u,J]),s.useEffect(()=>{if(!V)return;const a=window.setTimeout(()=>p(null),3200);return()=>window.clearTimeout(a)},[V]),s.useEffect(()=>()=>Z(),[]),s.useEffect(()=>{const a=Math.max(1,Math.ceil(K.length/ke));Ae>a&&de(a)},[K.length,Ae]);const Nt=u?`${L("/camera/stream")}?t=${te}`:"",be=c.autoExposure,qe=c.whiteBalanceMode==="manual",A=((Ra=n==null?void 0:n.info)==null?void 0:Ra.capabilities)??{},St=Array.isArray(A.awb_modes)&&A.awb_modes.length>0?A.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],kt=Array.isArray(A.noise_reduction_modes)&&A.noise_reduction_modes.length>0?A.noise_reduction_modes:["off","fast","high_quality"],Na=A.manual_digital_gain!==!1,Sa=!!((Ea=n==null?void 0:n.info)!=null&&Ea.night_mode),ka=u,ge=!M&&!u&&!(n!=null&&n.recording),_a=!M&&u,_t=!M&&!Je&&ka,Ct=!M&&!Ee&&ka,ve=Math.max(1,Math.ceil(K.length/ke)),ye=Math.min(Ae,ve),Ca=(ye-1)*ke,Rt=K.slice(Ca,Ca+ke);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full items-center justify-between gap-4",children:[e.jsx("div",{children:e.jsx("h1",{className:"font-headline text-xl font-bold text-primary",children:`OGScope ${t("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("zh"),children:t("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("en"),children:t("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Lt,{className:"h-3.5 w-3.5"})," ",t("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 xl:order-1 xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Qt.map(a=>e.jsxs("button",{type:"button",onClick:()=>void mt(a),className:`rounded border px-2 py-1 ${la===a?"border-primary text-primary":"border-outline-variant/30"}`,children:[a,"°"]},a))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void ja(!oe,ie),className:`rounded border px-2 py-1 ${oe?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void ja(oe,!ie),className:`rounded border px-2 py-1 ${ie?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void xt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ot,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ht(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Gt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:t("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:t("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void wa(!0),className:`rounded border px-2 py-1 ${Sa?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Ut,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void wa(!1),className:`rounded border px-2 py-1 ${Sa?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Jt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsx("input",{value:Pe,onChange:a=>pa(a.target.value),placeholder:t("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:xa,onChange:a=>ha(a.target.value),placeholder:t("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:Te||!Pe.trim(),onClick:()=>void ft(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[fa.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.presets.empty")}),fa.map(a=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:a.name}),e.jsx("div",{className:"text-on-surface-variant",children:a.description||t("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[t("cam.controls.exposure"),": ",a.exposure_us,"us | ",t("cam.controls.gain"),": ",a.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:Te,onClick:()=>void bt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:Te,onClick:()=>void gt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.delete")})]})]},a.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 xl:order-2 xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-3 gap-2 text-xs",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsxs("span",{className:"font-mono",children:[Number((g==null?void 0:g.cpu_usage)??0).toFixed(1),"%"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsxs("span",{className:"font-mono",children:[Number((g==null?void 0:g.memory_usage)??0).toFixed(1),"%"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsxs("span",{className:"font-mono",children:[Number((g==null?void 0:g.temperature)??0).toFixed(1),"°C"]})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:t("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"shrink-0 font-mono text-xs text-on-surface-variant",children:[t("cam.preview.state"),": ",t(u?"cam.state.streaming":"cam.state.idle")]})]}),re&&!u&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:"relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black",children:[u&&re&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),u?e.jsx("img",{ref:C,alt:"camera-preview",className:"h-full w-full object-contain",src:Nt,onLoad:()=>{H(null),z(!1),wt()},onError:()=>{var r;if(Z(),!D.current)return;const a=(r=C.current)==null?void 0:r.src;(async()=>{if(!D.current||!a)return;if(await Ja()==="busy"){H(t("cam.err.streamBusy")),z(!0);return}xe.current=window.setTimeout(()=>{D.current&&Ce(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ua,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:t("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:t("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"mr-1 inline h-3.5 w-3.5"}),t(M?"cam.btn.starting":"cam.btn.start")]})]}),(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(La,{className:"h-3.5 w-3.5 fill-current"})," ",t("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(ia/60).toString().padStart(2,"0")}:${(ia%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Ya(a=>!a),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:t(oa?"cam.hist.expand":"cam.hist.collapse")})}),!oa&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:na,onChange:a=>Ka(a.target.checked)})," ",t("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Me,onChange:a=>Xa(a.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Fe,onChange:a=>Za(a.target.checked)})," ",t("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:sa,onChange:a=>Qa(a.target.checked)})," ",t("cam.hist.over")]})]}),e.jsx("canvas",{ref:ue,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:$e.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:$e.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[$e.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_preview_fps)??at).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((Ma=n==null?void 0:n.info)==null?void 0:Ma.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(o==null?void 0:o.preview_target_fps)??(o==null?void 0:o.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((o==null?void 0:o.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((o==null?void 0:o.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(o==null?void 0:o.jpeg_source_format)??"RGB888"} / fail ${(o==null?void 0:o.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(o==null?void 0:o.preview_consumers)??0}/${(o==null?void 0:o.analysis_consumers)??0}/${(o==null?void 0:o.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((o==null?void 0:o.process_rss_kb)??0)/1024)} / ${Math.round(Number((o==null?void 0:o.process_swap_kb)??0)/1024)} MB`})]}),(o==null?void 0:o.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:t("cam.stats.longExposureThrottle",{exposure:Math.round(Number(o.actual_exposure_us??0)/1e3)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:he.current!=null?`${Math.max(0,Math.round((performance.now()-he.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((Fa=n==null?void 0:n.info)==null?void 0:Fa.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String((($a=n==null?void 0:n.info)==null?void 0:$a.driver)??(o==null?void 0:o.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Ba=n==null?void 0:n.info)==null?void 0:Ba.backend)??(o==null?void 0:o.camera_backend)??"—"} · lores ${(Pa=n==null?void 0:n.info)!=null&&Pa.lores_available||o!=null&&o.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((Ta=n==null?void 0:n.info)==null?void 0:Ta.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((Ia=n==null?void 0:n.info)==null?void 0:Ia.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Oa=n==null?void 0:n.info)==null?void 0:Oa.width)??"—"}x${((Da=n==null?void 0:n.info)==null?void 0:Da.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Aa=n==null?void 0:n.info)!=null&&Aa.auto_exposure?t("cam.controls.auto"):t("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!_a,onClick:()=>void st(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${_a?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Vt,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!_t,onClick:()=>void ot(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ua,{className:"inline h-4 w-4"})," ",t(Je?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!Ct,onClick:()=>void it(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(La,{className:"inline h-4 w-4"})," ",Ee?t("cam.btn.recordBusy"):n!=null&&n.recording?t("cam.btn.recordStop"):t("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:ba,onClick:()=>void ze(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.refresh")})}),nt&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:t("cam.files.loadingInfo")}),N&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(zt,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:N.filename})]}),e.jsx("button",{type:"button",onClick:()=>Ge(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":t("cam.files.closeDetail"),children:e.jsx(Kt,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[t("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Va(N.size)})]}),e.jsxs("div",{children:[t("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:N.type})]}),e.jsxs("div",{children:[t("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(N.modified).toLocaleString()})]}),N.exposure_us!=null&&e.jsxs("div",{children:[t("cam.controls.exposure"),": ",e.jsxs("span",{className:"font-mono",children:[N.exposure_us,"us"]})]}),N.analogue_gain!=null&&e.jsxs("div",{children:[t("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:N.analogue_gain})]}),N.resolution&&e.jsxs("div",{children:[t("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:N.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[K.length===0&&!ba&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.empty")}),Rt.map(a=>e.jsxs("div",{className:`rounded border p-2 ${le===a.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:a.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Va(a.size)," | ",new Date(a.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:a.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>yt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ht,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void vt(a.name),className:`rounded border px-2 py-1 ${le===a.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(qt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void jt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Pt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.delete")]})]})]},a.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ye<=1,onClick:()=>de(a=>Math.max(1,a-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.page",{current:ye,total:ve})}),e.jsx("button",{type:"button",disabled:ye>=ve,onClick:()=>de(a=>Math.min(ve,a+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 xl:col-span-3 xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.sensorFps"),e.jsx("input",{value:Xe,onChange:a=>{Ze(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.previewFps"),e.jsx("input",{value:Qe,onChange:a=>{Ye(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[t("cam.controls.sampling"),e.jsxs("select",{value:ta,onChange:a=>{ra(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[t("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Zt.map(a=>e.jsx("button",{type:"button",onClick:()=>{aa(a),G(!0)},className:`rounded border px-2 py-1 ${ea===a?"border-primary text-primary":"border-outline-variant/30"}`,children:a},a))})]}),e.jsx("button",{type:"button",disabled:!J,onClick:()=>void ct(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Wt,{className:"h-3.5 w-3.5"})," ",t("cam.controls.core")]}),ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:t("cam.controls.pendingChanges")}),be&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx($,{label:t("cam.controls.exposure"),value:c.exposure,min:100,max:12e4,step:100,unit:"us",disabled:be,onChange:a=>{f(!0),y(r=>({...r,exposure:a}))}}),e.jsx($,{label:t("cam.controls.gain"),value:c.gain,min:1,max:24,step:.1,disabled:be,onChange:a=>{f(!0),y(r=>({...r,gain:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.digitalGain"),value:c.digitalGain,min:1,max:8,step:.1,disabled:be||!Na,onChange:a=>{f(!0),y(r=>({...r,digitalGain:Number(a.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[t("cam.controls.noiseReductionMode"),e.jsx("select",{value:c.noiseReductionMode,onChange:a=>{f(!0),y(r=>({...r,noiseReductionMode:a.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:kt.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.nr.${a}`)},a))})]}),e.jsx($,{label:t("cam.controls.contrast"),value:c.contrast,min:0,max:2,step:.1,onChange:a=>{f(!0),y(r=>({...r,contrast:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.brightness"),value:c.brightness,min:-1,max:1,step:.1,onChange:a=>{f(!0),y(r=>({...r,brightness:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.saturation"),value:c.saturation,min:0,max:2,step:.1,onChange:a=>{f(!0),y(r=>({...r,saturation:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.sharpness"),value:c.sharpness,min:0,max:2,step:.1,onChange:a=>{f(!0),y(r=>({...r,sharpness:Number(a.toFixed(1))}))}})]}),!Na&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[t("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ha=n==null?void 0:n.info)==null?void 0:Ha.actual_digital_gain)??c.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!ce,onClick:()=>void lt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.autoExposure"),e.jsxs("select",{value:c.autoExposure?"auto":"manual",onChange:a=>{y(r=>({...r,autoExposure:a.target.value==="auto"})),f(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:t("cam.controls.auto")}),e.jsx("option",{value:"manual",children:t("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.colorMode"),e.jsxs("select",{value:c.colorMode,onChange:a=>{y(r=>({...r,colorMode:a.target.value})),f(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:t("cam.controls.color")}),e.jsx("option",{value:"mono",children:t("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.whiteBalance"),e.jsx("select",{value:c.whiteBalanceMode,onChange:a=>{y(r=>({...r,whiteBalanceMode:a.target.value})),f(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:St.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.wb.${a}`)},a))})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.aeFlicker"),e.jsxs("select",{value:c.aeFlickerMode,onChange:a=>{y(r=>({...r,aeFlickerMode:a.target.value})),f(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:t("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:1e4,max:1e7,step:1e4,value:c.autoExposureMaxUs,onChange:a=>{y(r=>({...r,autoExposureMaxUs:j(parseInt(a.target.value,10)||2e6,1e4,1e7)})),f(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx($,{label:"R Gain",value:c.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainR:Number(a.toFixed(1))})),f(!0)}}),e.jsx($,{label:"B Gain",value:c.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainB:Number(a.toFixed(1))})),f(!0)}})]}),!qe&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void dt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:t("cam.controls.applyMode")})})]})]})]})]}),(Re||V)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[Re&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:Re}),V&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:V})]})]})}Ft.createRoot(document.getElementById("root")).render(e.jsx($t.StrictMode,{children:e.jsx(Tt,{children:e.jsx(At,{children:e.jsx(Yt,{})})})})); diff --git a/web/static/analysis-lab/assets/http-VZMNcsmS.js b/web/static/analysis-lab/assets/http-VZMNcsmS.js deleted file mode 100644 index cb65497..0000000 --- a/web/static/analysis-lab/assets/http-VZMNcsmS.js +++ /dev/null @@ -1,11 +0,0 @@ -import{c as u}from"./index-C78KOEFu.js";import{r as o,j as f}from"./client-D1ZVDB-N.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 m=u("Camera",[["path",{d:"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z",key:"1tc9qg"}],["circle",{cx:"12",cy:"13",r:"3",key:"1vg3eu"}]]);/** - * @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 w=u("Save",[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]]),d=o.createContext(null),h=8e3;async function l(){const t=await fetch("/api/dev/system/info",{cache:"no-store"});let e={};try{e=await t.json()}catch{}if(!t.ok){const n=e;throw new Error(n.detail||`HTTP ${t.status}`)}return e}function S({children:t}){const[e,n]=o.useState(null),[i,r]=o.useState(null),a=o.useCallback(async()=>{try{r(null);const s=await l();n(s)}catch(s){r(s instanceof Error?s.message:String(s))}},[]);o.useEffect(()=>{a();const s=window.setInterval(()=>void a(),h);return()=>window.clearInterval(s)},[a]);const c=o.useMemo(()=>({info:e,error:i,refresh:a}),[e,i,a]);return f.jsx(d.Provider,{value:c,children:t})}function I(){const t=o.useContext(d);if(!t)throw new Error("useSystemInfo must be used within SystemInfoProvider");return t}async function y(t,e={}){const{cache:n,...i}=e,r={headers:{"Content-Type":"application/json"},...i};n!==void 0&&Object.assign(r,{cache:n});const a=await fetch(t,r);let c={};try{c=await a.json()}catch{}if(!a.ok){const s=c;throw new Error(s.detail||`HTTP ${a.status}`)}return c}async function g(t,e={}){const n=t.startsWith("/api/debug/")?t.replace("/api/debug/","/api/dev/debug/"):t;return y(n,e)}export{m as C,w as S,g as a,S as b,y as r,I as u}; diff --git a/web/static/analysis-lab/assets/http-lbh6zBy9.js b/web/static/analysis-lab/assets/http-lbh6zBy9.js new file mode 100644 index 0000000..7c4855d --- /dev/null +++ b/web/static/analysis-lab/assets/http-lbh6zBy9.js @@ -0,0 +1,16 @@ +import{c as u}from"./index-CV2LaaT8.js";import{r as o,j as f}from"./client-D1ZVDB-N.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 m=u("Camera",[["path",{d:"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z",key:"1tc9qg"}],["circle",{cx:"12",cy:"13",r:"3",key:"1vg3eu"}]]);/** + * @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 w=u("Save",[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]]);/** + * @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 S=u("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]),d=o.createContext(null),h=8e3;async function l(){const t=await fetch("/api/dev/system/info",{cache:"no-store"});let e={};try{e=await t.json()}catch{}if(!t.ok){const n=e;throw new Error(n.detail||`HTTP ${t.status}`)}return e}function k({children:t}){const[e,n]=o.useState(null),[i,r]=o.useState(null),a=o.useCallback(async()=>{try{r(null);const s=await l();n(s)}catch(s){r(s instanceof Error?s.message:String(s))}},[]);o.useEffect(()=>{a();const s=window.setInterval(()=>void a(),h);return()=>window.clearInterval(s)},[a]);const c=o.useMemo(()=>({info:e,error:i,refresh:a}),[e,i,a]);return f.jsx(d.Provider,{value:c,children:t})}function b(){const t=o.useContext(d);if(!t)throw new Error("useSystemInfo must be used within SystemInfoProvider");return t}async function y(t,e={}){const{cache:n,...i}=e,r={headers:{"Content-Type":"application/json"},...i};n!==void 0&&Object.assign(r,{cache:n});const a=await fetch(t,r);let c={};try{c=await a.json()}catch{}if(!a.ok){const s=c;throw new Error(s.detail||`HTTP ${a.status}`)}return c}async function I(t,e={}){const n=t.startsWith("/api/debug/")?t.replace("/api/debug/","/api/dev/debug/"):t;return y(n,e)}export{m as C,w as S,S as X,I as a,k as b,y as r,b as u}; diff --git a/web/static/analysis-lab/assets/index-BtAa6NQk.css b/web/static/analysis-lab/assets/index-BtAa6NQk.css new file mode 100644 index 0000000..79eb476 --- /dev/null +++ b/web/static/analysis-lab/assets/index-BtAa6NQk.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-3{left:.75rem}.left-\[10px\]{left:10px}.left-\[110px\]{left:110px}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-12{top:3rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-8{top:2rem}.top-\[18px\]{top:18px}.top-\[88px\]{top:88px}.top-\[94px\]{top:94px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.order-1{order:1}.order-3{order:3}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-0{height:0px}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100px\]{height:100px}.h-\[112px\]{height:112px}.h-\[152px\]{height:152px}.h-\[188px\]{height:188px}.h-\[460px\]{height:460px}.h-\[88px\]{height:88px}.h-\[calc\(100vh-7rem\)\]{height:calc(100vh - 7rem)}.h-dvh{height:100dvh}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[320px\]{max-height:320px}.max-h-\[420px\]{max-height:420px}.max-h-\[70vh\]{max-height:70vh}.max-h-\[min\(22rem\,55vh\)\]{max-height:min(22rem,55vh)}.max-h-\[min\(36vh\,18rem\)\]{max-height:min(36vh,18rem)}.max-h-\[min\(50vh\,28rem\)\]{max-height:min(50vh,28rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0px}.min-h-14{min-height:3.5rem}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[480px\]{min-height:480px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100px\]{width:100px}.w-\[200px\]{width:200px}.w-\[220px\]{width:220px}.w-\[360px\]{width:360px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[15rem\]{min-width:15rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[1880px\]{max-width:1880px}.max-w-\[220px\]{max-width:220px}.max-w-\[calc\(100\%-1\.5rem\)\]{max-width:calc(100% - 1.5rem)}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.border-separate{border-collapse:separate}.border-spacing-y-2{--tw-border-spacing-y: .5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-top-left{transform-origin:top left}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-px{--tw-translate-x: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-x-\[9px\]{border-left-width:9px;border-right-width:9px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-b-\[12px\]{border-bottom-width:12px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-2{border-right-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-amber-500\/40{border-color:#f59e0b66}.border-emerald-400\/40{border-color:#34d39966}.border-emerald-500\/20{border-color:#10b98133}.border-emerald-600\/35{border-color:#05966959}.border-error\/35{border-color:#ffb4ab59}.border-error\/40{border-color:#ffb4ab66}.border-error\/50{border-color:#ffb4ab80}.border-error\/60{border-color:#ffb4ab99}.border-outline-variant\/10{border-color:#4147541a}.border-outline-variant\/15{border-color:#41475426}.border-outline-variant\/20{border-color:#41475433}.border-outline-variant\/25{border-color:#41475440}.border-outline-variant\/30{border-color:#4147544d}.border-outline-variant\/40{border-color:#41475466}.border-primary{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.border-primary\/30{border-color:#adc6ff4d}.border-primary\/40{border-color:#adc6ff66}.border-primary\/50{border-color:#adc6ff80}.border-primary\/70{border-color:#adc6ffb3}.border-red-500\/40{border-color:#ef444466}.border-rose-400\/40{border-color:#fb718566}.border-sky-500\/30{border-color:#0ea5e94d}.border-teal-400\/75{border-color:#2dd4bfbf}.border-teal-500\/25{border-color:#14b8a640}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-white\/5{border-color:#ffffff0d}.border-x-transparent{border-left-color:transparent;border-right-color:transparent}.border-b-amber-400{--tw-border-opacity: 1;border-bottom-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-400\/95{background-color:#fbbf24f2}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-900\/55{background-color:#78350f8c}.bg-background{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-black\/25{background-color:#00000040}.bg-black\/35{background-color:#00000059}.bg-black\/40{background-color:#0006}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/80{background-color:#000c}.bg-emerald-800\/90{background-color:#065f46e6}.bg-error-container\/15{background-color:#93000a26}.bg-error-container\/20{background-color:#93000a33}.bg-error-container\/95{background-color:#93000af2}.bg-error\/10{background-color:#ffb4ab1a}.bg-error\/80{background-color:#ffb4abcc}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-neutral-950\/80{background-color:#0a0a0acc}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(173 198 255 / var(--tw-bg-opacity, 1))}.bg-primary-container{--tw-bg-opacity: 1;background-color:rgb(76 142 255 / var(--tw-bg-opacity, 1))}.bg-primary-container\/20{background-color:#4c8eff33}.bg-primary-container\/30{background-color:#4c8eff4d}.bg-primary-container\/50{background-color:#4c8eff80}.bg-primary\/10{background-color:#adc6ff1a}.bg-primary\/20{background-color:#adc6ff33}.bg-primary\/5{background-color:#adc6ff0d}.bg-red-500\/10{background-color:#ef44441a}.bg-secondary{--tw-bg-opacity: 1;background-color:rgb(64 229 108 / var(--tw-bg-opacity, 1))}.bg-secondary\/80{background-color:#40e56ccc}.bg-sky-300\/90{background-color:#7dd3fce6}.bg-sky-400{--tw-bg-opacity: 1;background-color:rgb(56 189 248 / var(--tw-bg-opacity, 1))}.bg-sky-400\/95{background-color:#38bdf8f2}.bg-sky-700\/80{background-color:#0369a1cc}.bg-sky-900\/50{background-color:#0c4a6e80}.bg-slate-500\/85{background-color:#64748bd9}.bg-slate-500\/90{background-color:#64748be6}.bg-slate-600\/40{background-color:#47556966}.bg-slate-600\/50{background-color:#47556980}.bg-slate-600\/60{background-color:#47556999}.bg-slate-900\/80{background-color:#0f172acc}.bg-slate-950\/50{background-color:#02061780}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-surface-container{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.bg-surface-container-high{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.bg-surface-container-high\/40{background-color:#272a3166}.bg-surface-container-high\/95{background-color:#272a31f2}.bg-surface-container-highest{--tw-bg-opacity: 1;background-color:rgb(50 53 60 / var(--tw-bg-opacity, 1))}.bg-surface-container-highest\/30{background-color:#32353c4d}.bg-surface-container-highest\/40{background-color:#32353c66}.bg-surface-container-highest\/95{background-color:#32353cf2}.bg-surface-container-low{--tw-bg-opacity: 1;background-color:rgb(25 28 34 / var(--tw-bg-opacity, 1))}.bg-surface-container-low\/80{background-color:#191c22cc}.bg-surface-container-low\/90{background-color:#191c22e6}.bg-surface-container-low\/95{background-color:#191c22f2}.bg-surface-container-lowest{--tw-bg-opacity: 1;background-color:rgb(11 14 20 / var(--tw-bg-opacity, 1))}.bg-surface-container-lowest\/90{background-color:#0b0e14e6}.bg-surface-container-lowest\/95{background-color:#0b0e14f2}.bg-surface-container\/50{background-color:#1d202680}.bg-surface-container\/60{background-color:#1d202699}.bg-surface-container\/80{background-color:#1d2026cc}.bg-surface-container\/90{background-color:#1d2026e6}.bg-violet-400{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity, 1))}.bg-violet-400\/95{background-color:#a78bfaf2}.bg-violet-900\/45{background-color:#4c1d9573}.bg-white\/5{background-color:#ffffff0d}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-300\/95{--tw-gradient-from: rgb(252 211 77 / .95) var(--tw-gradient-from-position);--tw-gradient-to: rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-emerald-600{--tw-gradient-from: #059669 var(--tw-gradient-from-position);--tw-gradient-to: rgb(5 150 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-600\/90{--tw-gradient-from: rgb(71 85 105 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/40{--tw-gradient-from: rgb(15 23 42 / .4) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/90{--tw-gradient-from: rgb(15 23 42 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-slate-800\/95{--tw-gradient-to: rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(30 41 59 / .95) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-slate-900\/70{--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(15 23 42 / .7) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-sky-400\/40{--tw-gradient-to: rgb(56 189 248 / .4) var(--tw-gradient-to-position)}.to-sky-500{--tw-gradient-to: #0ea5e9 var(--tw-gradient-to-position)}.to-slate-950{--tw-gradient-to: #020617 var(--tw-gradient-to-position)}.to-surface-container\/70{--tw-gradient-to: rgb(29 32 38 / .7) var(--tw-gradient-to-position)}.to-surface-container\/90{--tw-gradient-to: rgb(29 32 38 / .9) var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0\.5{padding-right:.125rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-body,.font-headline{font-family:system-ui,Segoe UI,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity, 1))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-200\/95{color:#fde68af2}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.text-on-primary-container{--tw-text-opacity: 1;color:rgb(0 40 93 / var(--tw-text-opacity, 1))}.text-on-surface{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.text-on-surface-variant{--tw-text-opacity: 1;color:rgb(194 198 214 / var(--tw-text-opacity, 1))}.text-on-surface-variant\/70{color:#c2c6d6b3}.text-on-surface-variant\/80{color:#c2c6d6cc}.text-on-surface-variant\/85{color:#c2c6d6d9}.text-on-surface-variant\/90{color:#c2c6d6e6}.text-on-surface-variant\/95{color:#c2c6d6f2}.text-on-surface\/80{color:#e1e2ebcc}.text-primary{--tw-text-opacity: 1;color:rgb(173 198 255 / var(--tw-text-opacity, 1))}.text-primary\/80{color:#adc6ffcc}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(64 229 108 / var(--tw-text-opacity, 1))}.text-sky-100{--tw-text-opacity: 1;color:rgb(224 242 254 / var(--tw-text-opacity, 1))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.text-sky-300\/90{color:#7dd3fce6}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-teal-100{--tw-text-opacity: 1;color:rgb(204 251 241 / var(--tw-text-opacity, 1))}.text-teal-200{--tw-text-opacity: 1;color:rgb(153 246 228 / var(--tw-text-opacity, 1))}.text-teal-200\/95{color:#99f6e4f2}.text-violet-200{--tw-text-opacity: 1;color:rgb(221 214 254 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-primary{accent-color:#adc6ff}.opacity-50{opacity:.5}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(255\,255\,255\,0\.6\)\]{--tw-shadow: 0 0 6px rgba(255,255,255,.6);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_20px_50px_rgba\(0\,0\,0\,0\.55\)\,inset_0_1px_0_rgba\(255\,255\,255\,0\.08\)\]{--tw-shadow: 0 20px 50px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);--tw-shadow-colored: 0 20px 50px var(--tw-shadow-color), inset 0 1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/35{--tw-ring-color: rgb(253 230 138 / .35)}.ring-sky-200\/30{--tw-ring-color: rgb(186 230 253 / .3)}.ring-sky-500\/25{--tw-ring-color: rgb(14 165 233 / .25)}.ring-slate-600\/50{--tw-ring-color: rgb(71 85 105 / .5)}.ring-teal-500\/10{--tw-ring-color: rgb(20 184 166 / .1)}.ring-violet-200\/35{--tw-ring-color: rgb(221 214 254 / .35)}.ring-white\/5{--tw-ring-color: rgb(255 255 255 / .05)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}html,body,#root{height:100%}.og-scrollbar{scrollbar-width:thin;scrollbar-color:#414754 #191c22}.og-scrollbar::-webkit-scrollbar{width:7px;height:7px}.og-scrollbar::-webkit-scrollbar-track{background:#191c22;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb{background:#414754;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb:hover{background:#5c6070}.hover\:border-primary:hover{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.hover\:bg-amber-500\/10:hover{background-color:#f59e0b1a}.hover\:bg-emerald-500\/10:hover{background-color:#10b9811a}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity, 1))}.hover\:bg-error-container\/20:hover{background-color:#93000a33}.hover\:bg-error-container\/30:hover{background-color:#93000a4d}.hover\:bg-error\/10:hover{background-color:#ffb4ab1a}.hover\:bg-rose-500\/10:hover{background-color:#f43f5e1a}.hover\:bg-sky-600:hover{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container:hover{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container-high:hover{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container\/80:hover{background-color:#1d2026cc}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-error:hover{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.hover\:text-on-surface:hover{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-4{gap:1rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:min-w-\[200px\]{min-width:200px}.md\:max-w-md{max-width:28rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-center{justify-content:center}.md\:gap-8{gap:2rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:ml-64{margin-left:16rem}.lg\:hidden{display:none}.lg\:max-w-lg{max-width:32rem}.lg\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:p-6{padding:1.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:max-w-\[340px\]{max-width:340px}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:gap-8{gap:2rem}.xl\:self-start{align-self:flex-start}}@media (min-width: 1536px){.\32xl\:static{position:static}.\32xl\:order-1{order:1}.\32xl\:order-2{order:2}.\32xl\:col-span-10{grid-column:span 10 / span 10}.\32xl\:col-span-2{grid-column:span 2 / span 2}.\32xl\:col-span-3{grid-column:span 3 / span 3}.\32xl\:col-span-9{grid-column:span 9 / span 9}.\32xl\:hidden{display:none}.\32xl\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\32xl\:self-start{align-self:flex-start}.\32xl\:shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none} diff --git a/web/static/analysis-lab/assets/index-C78KOEFu.js b/web/static/analysis-lab/assets/index-C78KOEFu.js deleted file mode 100644 index 269abd1..0000000 --- a/web/static/analysis-lab/assets/index-C78KOEFu.js +++ /dev/null @@ -1,26 +0,0 @@ -import{r as a,j as v}from"./client-D1ZVDB-N.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 f=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),u=(...e)=>e.filter((s,t,o)=>!!s&&s.trim()!==""&&o.indexOf(s)===t).join(" ").trim();/** - * @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. - */var w={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** - * @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 S=a.forwardRef(({color:e="currentColor",size:s=24,strokeWidth:t=2,absoluteStrokeWidth:o,className:l="",children:r,iconNode:m,...n},c)=>a.createElement("svg",{ref:c,...w,width:s,height:s,stroke:e,strokeWidth:o?Number(t)*24/Number(s):t,className:u("lucide",l),...n},[...m.map(([i,d])=>a.createElement(i,d)),...Array.isArray(r)?r:[r]]));/** - * @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 P=(e,s)=>{const t=a.forwardRef(({className:o,...l},r)=>a.createElement(S,{ref:r,iconNode:s,className:u(`lucide-${f(e)}`,o),...l}));return t.displayName=`${e}`,t};/** - * @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 A=P("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]),k={"app.title":"OGScope Plate Solve Console","nav.lab":"Lab","nav.labImage":"Image solve","nav.labVideo":"Video solve","delete.uploadCascade":"Delete {n} linked experiment record(s) as well?","lab.solveCurrentFrame":"Solve current frame (file)","lab.solveFileStart":"Start continuous file solve","lab.solveFileStop":"Stop continuous file solve","lab.cameraPreviewLoading":"Connecting to shared preview…","lab.solveCameraFrame":"Solve live camera frame","lab.solveCameraStart":"Start camera solve","lab.solveCameraStop":"Stop camera solve","lab.videoPreviewFailed":"This video format may be unsupported by the browser. Try MP4 (H.264) or WebM.","lab.previewModeFile":"Pool file","lab.previewModeCamera":"Device camera","lab.videoLiveIntro":"Shares the same camera as Camera Debug — preview and solve live frames here without opening debug. Both pages can run together.","lab.videoMjpegHint":"MJPEG slots are full: close live previews on other pages or retry later (default ~4; tune OGSCOPE_STREAM_MAX_MJPEG_CLIENTS in systemd).","lab.cameraStreamStatusFail":"Could not read MJPEG slot status. Please retry shortly.","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"Raw Prob","lab.systemLoad":"System load","results.saveBatchAll":"Save all to records","nav.pool":"Assets","nav.history":"Records","nav.cameraDebug":"Camera Debug","nav.home":"Home","nav.systemAdmin":"Back to System Admin","lang.zh":"中文","lang.en":"EN","sidebar.assets":"Uploaded assets","sidebar.upload":"Upload","sidebar.refresh":"Refresh","sidebar.debugCaptures":"Debug console media","sidebar.assetTypeImage":"Image","sidebar.assetTypeVideo":"Video","sidebar.debugEmpty":"No debug files","sidebar.importToPool":"Import to pool","sidebar.importToPoolWithTranscode":"Transcode & Import","sidebar.importToPoolDirect":"Import Directly","sidebar.flowPreparing":"Preparing...","sidebar.flowImportingDebug":"Importing debug capture...","sidebar.flowDownloadingPool":"Downloading imported file...","sidebar.flowWritingBuffer":"Writing to transcoder buffer...","sidebar.flowLoadingTranscoder":"Loading transcoder...","sidebar.flowTranscoding":"Transcoding AVI -> MP4...","sidebar.flowPackaging":"Packaging output...","sidebar.flowUploadingMp4":"Uploading MP4...","sidebar.flowReplacing":"Replacing and removing original AVI...","sidebar.flowNoTranscode":"No transcode required, imported directly.","sidebar.flowDone":"Completed","sidebar.flowDoneMsg":"Done: {name}","sidebar.flowFailed":"Failed","sidebar.debugPage":"Page {cur} / {total}","sidebar.batchPresets":"Batch presets","sidebar.batchHint":"Check presets, then use Batch solve to compare multiple param sets.","lab.selectOrUpload":"Pick an uploaded or imported asset from the left","lab.selectOrUploadVideo":"Pick a pool video to preview, or use the button above for the live camera frame.","lab.file":"File","lab.source":"Source","lab.layers":"Layers","lab.layer.matched":"Matched","lab.layer.pattern":"Pattern","lab.layer.all":"All centroids","lab.layer.rejected":"Rejected (red cross)","lab.previewConfidence":"Confidence","lab.grid":"Grid","lab.zoomIn":"Zoom in","lab.zoomOut":"Zoom out","lab.zoomReset":"Reset","lab.resolution":"Resolution","lab.fwhm":"FWHM","lab.starsDetected":"Stars detected","lab.meta.title":"Capture & file info","lab.meta.noSidecar":"No sidecar (not from debug capture)","lab.meta.partial":"No detailed sidecar; file info only.","lab.solveSection":"Solve","lab.imageSection":"Image","lab.metric.solveMs":"Time","lab.metric.solveComputeMs":"Solve compute","lab.metric.solveComputeHelp":"Server-side Tetra3 + star extraction only (no network).","lab.metric.solveRoundTripMs":"End-to-end","lab.metric.solveRoundTripHelp":"From request start to UI updated: network + JSON + render.","lab.metric.backendTotalMs":"Backend total","lab.metric.openDecodeMs":"Open/decode","lab.metric.preprocessMs":"Preprocess","lab.metric.extractMs":"Extract","lab.metric.solveOnlyMs":"Solve match","lab.metric.probHelp":"Tetra3 `Prob` is false-positive probability (lower is better). We use (1−Prob)×100% when representable; for extremely small Prob, IEEE doubles round to 100%, so we map −log10(Prob) to ~70–100% for differentiation.","lab.metric.probRawHelp":"Raw Tetra3 Prob (e.g. log-likelihood); compare with the normalized line above.","lab.metric.radec":"RA / Dec","lab.metric.matches":"Matches","lab.metric.rmse":"RMSE","lab.metric.prob":"Prob.","lab.metric.status":"Status","lab.gateNextMs":"Retry in ~{ms} ms","lab.gate.skipRecording":"Recording is active: the camera is writing video, so live-frame solve is paused. Stop recording and try again.","lab.gate.skipBusyInFlight":"The previous solve is still running; please wait. If this repeats often, increase the realtime solve interval.","lab.gate.skipInterval":"Solve requests are arriving faster than allowed; the server is throttling. Wait for the countdown and retry.","lab.gate.skipBusyFallback":"Realtime solve could not start ({detail}).","lab.centroidQualityTitle":"Centroid quality","lab.centroidQualityMetrics":"In {in} → kept {out}; removed dense {dense}, collinear {line}","meta.exposure":"Exposure","meta.gain":"Gain","meta.fps":"FPS","meta.sensor":"Sensor","meta.colorMode":"Color","meta.outputResolution":"Output size","meta.fileTime":"File time","meta.fileSize":"File size","results.viewRaw":"Raw JSON","results.hideRaw":"Hide","results.solveHistoryTitle":"Solve history","results.historySingle":"Single frame","results.historyBatch":"Batch ({count} runs)","params.title":"Solve parameters","params.blockSolveIntro":"Plate-solve (Tetra3): FOV, timeout, and coarse sky hints. FOV should match your lens.","params.centroid":"Star detection","params.blockCentroidIntro":"Star detection: threshold, blob area, and local background window for centroids.","params.fov":"FOV estimate (°)","params.fovHelp":"Horizontal field of view for lost-in-space solve.","params.fovErr":"FOV max error (°)","params.fovErrHelp":"Search range around estimated FOV.","params.timeout":"Timeout (ms)","params.timeoutHelp":"Max wait time per solve.","params.solveIntervalMs":"Realtime solve interval (ms)","params.solveIntervalMsHelp":"Desired interval is adjustable, but backend clamps it into a safe range.","params.solveIntervalIndependent":"Solve cadence is independent from sensor and preview frame rates.","params.solveIntervalBound":"Backend bounds: {min}-{max} ms, effective now: {effective} ms","params.centroidRejectionLevel":"Centroid rejection (1–5)","params.centroidRejectionLevelHelp":"1 keeps more stars; 5 aggressively rejects dense clusters and collinear false detections; default 3.","params.centroidRejectionScale":"Conservative ← → Aggressive","params.solveProfile":"Solve profile","params.solveProfileHelp":"Speed/Balanced/Robust tune timeout, centroid thresholds, and matching star count together.","params.solveProfileSpeed":"Speed first","params.solveProfileBalanced":"Balanced","params.solveProfileRobust":"Robust first","params.ra":"RA hint (°)","params.raHelp":"Approximate right ascension in degrees.","params.dec":"Dec hint (°)","params.decHelp":"Approximate declination in degrees.","params.maxSide":"Max long side before extract (px)","params.maxSideHelp":"Downscale long edge for faster centroid extraction.","params.detailLevelFull":"Include full Tetra3 raw block (larger payload, for debugging only).","params.largeScaleBg":"Large-scale background flattening","params.largeScaleBgHelp":"Before centroiding, estimate a low-frequency background on a downscaled image and correct uneven illumination (e.g. corner glow). Off by default to match legacy behavior.","params.sigma":"σ threshold","params.sigmaHelp":"Multiplier over background noise for star candidates.","params.maxArea":"max_area","params.maxAreaHelp":"Max connected component area in pixels.","params.minArea":"min_area","params.minAreaHelp":"Min connected component area in pixels.","params.filtsize":"filtsize (odd)","params.filtsizeHelp":"Local filter window size, must be odd.","btn.solveOne":"Solve once","btn.solveBatch":"Batch solve (presets)","btn.applyPresets":"Apply preset to form","btn.savePreset":"Save","placeholder.newPreset":"New preset name","pool.title":"Server asset pool","pool.col.name":"Filename","pool.col.source":"Source","pool.col.size":"Size","pool.col.time":"Modified","pool.delete":"Delete","history.title":"Experiment records","history.intro":"Saved solve snapshots from the Lab. After a solve, use Save to records in the Lab main panel (Result comparison), or Save on each batch result card. Search by filename or preset; export JSON/CSV for backup.","history.search":"Search…","history.searchBtn":"Search","history.exportJson":"Export JSON","history.exportCsv":"Export CSV","history.total":"Total {n}","history.preset":"Preset","history.metrics":"Metrics","history.detail":"Details","history.collapse":"Collapse","history.prev":"Prev","history.next":"Next","history.delete":"Delete","delete.uploadFirst":'Delete "{name}" from the asset pool?',"delete.uploadSecond":"This cannot be undone. Confirm again?","delete.experimentFirst":"Delete this experiment record?","delete.experimentSecond":"This cannot be undone. Confirm again?","results.title":"Results","results.saveCurrent":"Save to records","results.saveRow":"Save","results.expand":"Expand","results.collapseJson":"Collapse","err.selectFile":"Select a file","err.selectPresets":"Select at least one preset","common.placeholder":"—","lab.transcode.title":"AVI requires transcoding","lab.transcode.desc":"This video is AVI. For browser preview and continuous solving, transcode it locally to MP4 and upload replacement. The original AVI on server will be removed after success.","lab.transcode.button":"Transcode and Upload Replacement","lab.transcode.loading":"Loading transcoder...","lab.transcode.writingBuffer":"Writing to transcoder buffer...","lab.transcode.running":"Transcoding AVI -> MP4...","lab.transcode.packaging":"Packaging transcoded output...","lab.transcode.uploading":"Uploading transcoded output...","lab.transcode.done":"Transcode and replacement completed.","lab.transcode.failed":"Transcode or upload failed. Please retry.","lab.transcode.fetchFailed":"Failed to fetch AVI from server.","sys.shell.subtitle":"system debug console","sys.shell.nav.overview":"Overview","sys.shell.nav.network":"Network","sys.shell.nav.camera":"Camera Debug","sys.shell.nav.analysis":"Analysis Console","sys.shell.nav.sensors":"Sensors","sys.shell.nav.power":"Power","sys.shell.nav.hmi":"HMI","sys.shell.nav.config":"Config","sys.shell.workbench":"WORKBENCH / System workspace","sys.shell.node":"Node","sys.shell.top.overview":"System Status","sys.shell.top.network":"Network & WiFi","sys.shell.top.sensors":"Sensors","sys.shell.top.power":"Power Management","sys.shell.top.hmi":"HMI","sys.shell.top.config":"Config","sys.overview.breadcrumb.console":"Console","sys.overview.breadcrumb.module":"System Overview","sys.overview.title":"System Overview","sys.overview.subtitle":"Live system telemetry snapshot","sys.overview.metric.cpu":"CPU Usage","sys.overview.metric.mem":"Memory Usage","sys.overview.metric.temp":"Core Temperature","sys.overview.metric.wifi":"WiFi Link","sys.overview.metric.uptime":"Uptime","sys.overview.metric.load":"Load (1m)","sys.overview.metric.storage":"Storage","sys.overview.linkActive":"Link Active","sys.overview.tempState":"status: nominal","sys.overview.wifiSummary":"WiFi Summary","sys.overview.iface":"Interface","sys.overview.signal":"Signal","sys.overview.quality":"Quality","sys.overview.storageComingSoon":"Coming soon","sys.logs.title":"System Logs","sys.logs.kernel":"Kernel","sys.logs.liveToggle":"Live pull","sys.logs.refresh":"Refresh","sys.logs.liveOffHint":"Live pull is off (default)","sys.logs.loading":"Loading logs...","sys.logs.empty":"No logs","sys.placeholder.breadcrumb":"Console / Reserved Module","sys.placeholder.block":"PLANNED BLOCK","sys.placeholder.desc":"Shell layout is reserved and ready for real data integration.","sys.placeholder.status":"STATUS: RESERVED / Structured placeholder page.","sys.placeholder.sensors.title":"Sensors Console","sys.placeholder.sensors.desc":"Unified monitor for IMU, environment, power rails, and timeline sampling.","sys.placeholder.sensors.block1":"Realtime chart area","sys.placeholder.sensors.block2":"Device health cards","sys.placeholder.sensors.block3":"Threshold alerts","sys.sensors.title":"Sensors Console","sys.sensors.desc":"Visual compass (magnetometer) and gyro rate gauges; backed by dev debug APIs.","sys.sensors.mag.section":"Magnetometer (AK09911 / I²C)","sys.sensors.mag.note":"Defaults: bus 1, address 12 (0x0C with CAD to GND). No app-level GPIO mapping is required; enable I²C in firmware and load i2c-dev.","sys.sensors.mag.bus":"I²C bus","sys.sensors.mag.addr":"7-bit address (decimal, 12 = 0x0C)","sys.sensors.mag.i2cdetect":"Run i2cdetect","sys.sensors.mag.btnSelftest":"Run self-test","sys.sensors.mag.btnProbe":"Probe all buses","sys.sensors.mag.btnCalStart":"Start heading calibration","sys.sensors.mag.btnCalCommit":"Save & lock heading","sys.sensors.mag.btnCalReset":"Reset to auto mode","sys.sensors.mag.btnCalStatus":"Calibration status","sys.sensors.mag.running":"Running…","sys.sensors.mpu.section":"IMU (MPU-6050 / I²C)","sys.sensors.mpu.note":"Defaults: bus 1, address 104 (0x68 with AD0 low). Includes gyro angular-rate sampling (MPU integrates accel + gyro).","sys.sensors.mpu.addr":"7-bit address (decimal, 104 = 0x68)","sys.sensors.mpu.btnSelftest":"Run MPU self-test","sys.sensors.mpu.running":"Running…","sys.sensors.gyro.title":"Gyroscope (MPU-6050)","sys.sensors.gyro.subtitle":"Uses the same bus/address as above; one IMU read: 3-axis gyro, accel-derived roll/pitch, and a 3D board hint.","sys.sensors.gyro.btn":"Read angular rate","sys.sensors.gyro.loading":"Sampling…","sys.sensors.gyro.dps":"Angular rate","sys.sensors.gyro.raw":"Raw","sys.sensors.gyro.unitDps":"°/s, ±250 °/s range, default sensitivity","sys.sensors.gyro.unitRaw":"signed 16-bit","sys.sensors.gyro.hint":"Click “Read angular rate” to sample accel (0x3B) and gyro (0x43). Roll/pitch use gravity; ωz is yaw rate, not absolute heading.","sys.sensors.gyro.errUnknown":"Sample failed","sys.sensors.gyro.live":"Live update (~2 Hz)","sys.sensors.gyro.barsTitle":"Angular rate bars (±250 °/s full scale)","sys.sensors.gyro.rawBlock":"Raw register values (16-bit)","sys.sensors.gyro.att3dTitle":"3D attitude hint","sys.sensors.gyro.att3dDesc":"Roll/pitch from accelerometer (gravity); ωz is yaw rate (not magnetic heading). Body XYZ triad is fixed to the PCB and rotates in 3D.","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"Body frame on the PCB: +X amber, +Y sky, +Z violet (normal); short dashes mark negative directions; the whole frame follows roll/pitch.","sys.sensors.gyro.att3dRefTitle":"Orthogonal axes (fixed view)","sys.sensors.gyro.att3dRefDesc":"Same colors as left; use this to read XYZ directions at a glance.","sys.sensors.gyro.roll":"Roll","sys.sensors.gyro.pitch":"Pitch","sys.sensors.gyro.yawRate":"Yaw rate","sys.sensors.compass.tapeCaption":"Heading tape · center = current readout","sys.sensors.compass.title":"Magnetometer · North compass","sys.sensors.compass.desc":"Horizontal-plane heading for “roughly north” debugging; precise alignment needs calibration and a fixed mount definition.","sys.sensors.compass.btn":"Refresh compass","sys.sensors.compass.loading":"Measuring…","sys.sensors.compass.live":"Live update","sys.sensors.compass.headingLabel":"Horizontal heading","sys.sensors.compass.headingHint":"From atan2(Hx, Hy), degrees","sys.sensors.compass.cardN":"N","sys.sensors.compass.cardE":"E","sys.sensors.compass.cardS":"S","sys.sensors.compass.cardW":"W","sys.sensors.compass.err":"Sample failed","sys.sensors.compass.footnote":"The red tip tracks the horizontal XY field direction; it should move as you rotate the board. Use soft/hard-iron calibration for tight north lock.","sys.sensors.jsonToggle":"Raw JSON (troubleshooting)","sys.hmi.title":"HMI · SPI display","sys.hmi.desc":"ST7796 via hardware plane (default 320×320). Requires OGSCOPE_DISPLAY_ENABLED=true and SPI enabled (/dev/spidev0.0).","sys.hmi.status.section":"Display & hardware plane","sys.hmi.status.refresh":"Refresh","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"present","sys.hmi.status.no":"missing","sys.hmi.status.resolution":"Resolution / DC","sys.hmi.status.driver":"Driver handle","sys.hmi.status.open":"open","sys.hmi.status.closed":"closed","sys.hmi.status.screenOutput":"Logical output","sys.hmi.status.on":"on","sys.hmi.status.off":"off","sys.hmi.status.lastPattern":"Last pattern","sys.hmi.status.lastError":"Last error","sys.hmi.actions.section":"Output to panel","sys.hmi.actions.hint":"Uses hardware plane device.command → hmi; large frames use a longer RPC timeout.","sys.hmi.actions.smoke":"Test pattern (text + frame)","sys.hmi.actions.colorbars":"Color bars","sys.hmi.actions.fill":"Fill RGB","sys.hmi.actions.screenOn":"Enable panel output","sys.hmi.actions.screenOff":"Pause panel output","sys.hmi.actions.release":"Release driver (close SPI/GPIO)","sys.hmi.rawJson":"Raw JSON response","sys.placeholder.hmi.title":"HMI Console","sys.placeholder.hmi.desc":"Debug workspace for SPI display, key matrix, backlight, and UI replay.","sys.placeholder.hmi.block1":"Display preview","sys.placeholder.hmi.block2":"Input event stream","sys.placeholder.hmi.block3":"Mode controls","sys.placeholder.power.title":"Power Console","sys.placeholder.power.desc":"Debug workspace for battery, charging, power profile, and thermal policy.","sys.placeholder.power.block1":"Power dashboard","sys.placeholder.power.block2":"Power policy table","sys.placeholder.power.block3":"Recovery actions","cam.title":"Camera Debug Console","cam.subtitle":"Migrated to unified admin UI with key optimizations preserved","cam.btn.system":"System Admin","cam.btn.analysis":"Analysis Console","cam.btn.home":"Home","cam.btn.start":"Start Preview","cam.btn.starting":"Starting...","cam.btn.stop":"Stop Preview","cam.btn.stopping":"Stopping...","cam.btn.capture":"Capture","cam.btn.capturing":"Capturing...","cam.btn.recordStart":"Start Recording","cam.btn.recordStop":"Stop Recording","cam.btn.recordBusy":"Switching recording...","cam.btn.refresh":"Refresh Status","cam.preview.title":"Live Preview","cam.preview.state":"State","cam.preview.mode":"Exposure Mode","cam.preview.emptyTitle":"Preview is not started","cam.preview.emptyDesc":"Start the camera preview stream with the button below.","cam.state.streaming":"streaming","cam.state.idle":"idle","cam.state.rec":"REC","cam.stats.requestFps":"Request FPS","cam.hint.mjpegSingleStream":"MJPEG concurrent streams are limited (default ~2). Beyond the cap, extra tabs may contend or get busy—reduce simultaneous live previews.","cam.stats.frameFps":"Display FPS","cam.stats.fpsMeasureNote":"Pixel deltas on a 32×32 sample over ~1s. Multiple previews, slow links, or a stream cap below the sensor target often read much lower.","cam.err.streamBusy":"Preview unavailable: MJPEG slot is already in use","cam.err.streamBusyHint":"Close the live preview in other tabs or in the Analysis console, then try again.","cam.err.streamProbeFailed":"Could not open the preview stream","cam.err.streamProbeDetail":"Check network and camera service, or retry in a moment.","cam.stats.targetFps":"Camera capture target FPS","cam.stats.streamPacingFps":"Shared stream / MJPEG cap FPS","cam.stats.streamPacingHint":"Matches OGSCOPE_SHARED_PREVIEW_FPS: shared grabber pacing and per-stream MJPEG min frame spacing.","cam.stats.captureFps":"Actual capture FPS","cam.stats.encodeMs":"Average JPEG encode","cam.stats.encoder":"Preview encoder","cam.stats.driver":"Camera driver","cam.stats.metadata":"Metadata","cam.stats.consumers":"Preview/analysis/record consumers","cam.stats.cameraMemory":"Process memory/swap","cam.stats.longExposureThrottle":"Auto exposure is about {exposure} ms, so long exposure is limiting FPS; exposure is not shortened to preserve analysis quality.","cam.stats.uptime":"Stream Uptime","cam.controls.title":"Controls","cam.controls.core":"Core Settings","cam.controls.fps":"FPS","cam.controls.sensorFps":"Sensor target FPS","cam.controls.previewFps":"Preview target FPS","cam.controls.resolution":"Resolution","cam.controls.applyRes":"Apply resolution","cam.controls.sampling":"Sampling mode","cam.controls.exposure":"Exposure","cam.controls.gain":"Analog Gain","cam.controls.digitalGain":"Digital Gain","cam.controls.noiseReduction":"Noise Reduction","cam.controls.noiseReductionMode":"Noise Reduction","cam.controls.digitalGainReadOnly":"Digital gain is read-only on this driver","cam.controls.aeFlicker":"AE Flicker","cam.controls.maxAeFrame":"Max AE frame (us)","cam.controls.contrast":"Contrast","cam.controls.brightness":"Brightness","cam.controls.saturation":"Saturation","cam.controls.sharpness":"Sharpness","cam.controls.applySettings":"Apply All Settings","cam.controls.applyAll":"Save Parameters","cam.controls.applyRuntime":"Save Runtime Settings","cam.controls.mode":"Mode Controls","cam.controls.autoExposure":"Exposure Mode","cam.controls.whiteBalance":"White Balance","cam.controls.colorMode":"Color Mode","cam.controls.auto":"Auto","cam.controls.manual":"Manual","cam.controls.night":"Night","cam.controls.off":"Off","cam.controls.nr.off":"Off","cam.controls.nr.fast":"Fast","cam.controls.nr.high_quality":"High quality","cam.controls.wb.auto":"Auto","cam.controls.wb.daylight":"Daylight","cam.controls.wb.cloudy":"Cloudy","cam.controls.wb.tungsten":"Tungsten","cam.controls.wb.fluorescent":"Fluorescent","cam.controls.wb.indoor":"Indoor","cam.controls.wb.manual":"Manual","cam.controls.wb.night":"Night","cam.controls.color":"Color","cam.controls.mono":"Mono","cam.controls.applyAe":"Apply Exposure Mode","cam.controls.applyWb":"Apply White Balance","cam.controls.applyColor":"Apply Color Mode","cam.controls.applyMode":"Apply Mode Settings","cam.controls.lockedByAe":"Exposure and gain are locked while auto exposure is enabled.","cam.controls.lockedByWb":"R/B gains are locked unless white balance is in manual mode.","cam.controls.pendingChanges":"You have unapplied parameter changes","cam.controls.tools":"Tools","cam.controls.nightPreset":"Night Preset","cam.controls.nightOn":"Enable Night Mode","cam.controls.nightOff":"Disable Night Mode","cam.controls.backup":"Backup Settings","cam.controls.restore":"Restore Settings","cam.controls.reset":"Reset Camera","cam.quick.title":"Quick Presets & Smart Tuning","cam.quick.daylight":"Daylight","cam.quick.night":"Night","cam.quick.nightPreset":"Apply Night Preset","cam.quick.deep-sky":"Deep Sky","cam.quick.planetary":"Planetary","cam.quick.autoAdjust":"Auto Adjust","cam.quick.nightHint":"Night preset is one-click tuning; night mode toggle is runtime on/off control.","cam.system.title":"System Monitor","cam.system.status":"Camera Ready","cam.system.stream":"Stream","cam.system.sensor":"Sensor","cam.system.quality":"Image Quality","cam.presets.title":"Preset Management","cam.presets.name":"Preset Name","cam.presets.desc":"Preset Description","cam.presets.save":"Save Preset","cam.presets.empty":"No presets","cam.presets.noDesc":"No description","cam.presets.apply":"Apply","cam.presets.delete":"Delete","cam.files.title":"File Management","cam.files.refresh":"Refresh File List","cam.files.empty":"No files","cam.files.download":"Download","cam.files.info":"Details","cam.files.delete":"Delete","cam.files.loadingInfo":"Loading file details...","cam.files.prev":"Prev","cam.files.next":"Next","cam.files.page":"Page {current}/{total}","cam.files.closeDetail":"Close details","cam.files.size":"Size","cam.files.type":"Type","cam.files.modified":"Modified","cam.hist.title":"Histogram","cam.hist.enabled":"Enable histogram","cam.hist.luminance":"Luminance","cam.hist.over":"Over-exposure","cam.hist.expand":"Expand Histogram","cam.hist.collapse":"Collapse Histogram","cam.notice.previewStart":"Preview started","cam.notice.previewStop":"Preview stopped","cam.notice.captureSaved":"Capture saved: {name}","cam.notice.recordStart":"Recording started: {name}","cam.notice.recordStop":"Recording stopped","cam.notice.fpsApplied":"FPS updated to {fps}","cam.notice.resApplied":"Resolution updated to {w}x{h}","cam.notice.samplingApplied":"Sampling mode updated: {mode}","cam.notice.settingsApplied":"Settings applied","cam.notice.aeApplied":"Exposure mode applied","cam.notice.wbApplied":"White balance applied","cam.notice.colorApplied":"Color mode applied","cam.notice.modeApplied":"Mode settings applied","cam.notice.runtimeApplied":"Runtime settings applied","cam.notice.rotationApplied":"Rotation set to {value}°","cam.mirror.hint":"Use with rotation when the camera is mounted inverted; preview, plate solve, and polar guide share one coordinate frame.","cam.mirror.horizontal":"Horizontal mirror","cam.mirror.vertical":"Vertical mirror","cam.notice.mirrorApplied":"Mirror settings updated","cam.notice.nightOn":"Night mode enabled","cam.notice.nightOff":"Night mode disabled","cam.notice.nightPreset":"Night preset applied","cam.notice.reset":"Camera settings reset","cam.notice.backup":"Settings backed up","cam.notice.restore":"Settings restored","cam.notice.presetSaved":"Preset saved: {name}","cam.notice.presetApplied":"Preset applied: {name}","cam.notice.presetDeleted":"Preset deleted: {name}","cam.notice.download":"Download started: {name}","cam.notice.downloadWithSidecar":"Downloaded: {name} (with sidecar {sidecar})","cam.notice.fileDeleted":"File deleted: {name}","cam.notice.quickPresetApplied":"Quick preset applied: {preset}","cam.notice.needPreview":"Start preview first","cam.notice.needManualForAutoAdjust":"Switch to manual exposure before auto-adjust","cam.notice.autoAdjusted":"Auto-adjust completed and applied","cam.confirm.deletePreset":"Delete preset {name}?","cam.confirm.deleteFile":"Delete file {name}?","sys.sensors.mag.calStatusPrefix":"Calibration mode:","sys.sensors.mag.calModeAuto":"Auto (not locked)","sys.sensors.mag.calModeRecording":"Recording (rotate device)","sys.sensors.mag.calModeLocked":"Locked (saved heading)","sys.sensors.mag.calSamplesPrefix":"Recorded samples:","sys.sensors.mag.calRecordingHint":"Tip: keep level and rotate slowly; save after 10+ samples.","sys.sensors.mag.calLockedHint":"Locked heading params:"},C={"app.title":"OGScope 星空解算控制台","nav.lab":"解算台","nav.labImage":"图片解算","nav.labVideo":"视频解算","nav.pool":"素材池","nav.history":"实验记录","nav.cameraDebug":"相机调试控制台","nav.home":"首页","nav.systemAdmin":"返回系统后台","lang.zh":"中文","lang.en":"EN","sidebar.assets":"自行上传素材","sidebar.upload":"上传文件","sidebar.refresh":"刷新列表","sidebar.debugCaptures":"调试控制台素材","sidebar.assetTypeImage":"图片","sidebar.assetTypeVideo":"视频","sidebar.debugEmpty":"暂无调试文件","sidebar.importToPool":"导入到素材池","sidebar.importToPoolWithTranscode":"转码并导入素材池","sidebar.importToPoolDirect":"直接导入素材池","sidebar.flowPreparing":"准备中…","sidebar.flowImportingDebug":"正在导入调试素材…","sidebar.flowDownloadingPool":"正在下载已导入文件…","sidebar.flowWritingBuffer":"正在写入转码缓冲区…","sidebar.flowLoadingTranscoder":"正在加载转码器…","sidebar.flowTranscoding":"正在转码 AVI -> MP4…","sidebar.flowPackaging":"正在封装转码结果…","sidebar.flowUploadingMp4":"正在上传 MP4…","sidebar.flowReplacing":"正在替换并清理原 AVI…","sidebar.flowNoTranscode":"该文件无需转码,已直接导入。","sidebar.flowDone":"流程完成","sidebar.flowDoneMsg":"已完成:{name}","sidebar.flowFailed":"流程失败","sidebar.debugPage":"第 {cur} / {total} 页","sidebar.batchPresets":"批量预设","sidebar.batchHint":"勾选后点击「批量解算」可一次用多组参数对比结果。","lab.selectOrUpload":"从左侧选择自行上传或已导入的素材","lab.selectOrUploadVideo":"从左侧选择视频素材预览;或使用上方按钮解算设备相机实时帧。","lab.file":"文件","lab.source":"来源","lab.layers":"叠加层","lab.layer.matched":"匹配星","lab.layer.pattern":"图案星","lab.layer.all":"全部质心","lab.layer.rejected":"已剔除(红叉)","lab.previewConfidence":"置信度","lab.grid":"网格","lab.zoomIn":"放大","lab.zoomOut":"缩小","lab.zoomReset":"复位","lab.resolution":"分辨率","lab.fwhm":"FWHM","lab.starsDetected":"检测星点","lab.meta.title":"拍摄与文件信息","lab.meta.noSidecar":"无侧车信息(非调试采集或仅本地上传)","lab.meta.partial":"暂无侧车详细字段,仅显示文件信息。","lab.solveSection":"解算","lab.imageSection":"图像","lab.metric.solveMs":"用时","lab.metric.solveComputeMs":"解算计算用时","lab.metric.solveComputeHelp":"服务端 Tetra3 与提星等纯计算耗时(与网络无关)。","lab.metric.solveRoundTripMs":"全链路用时","lab.metric.solveRoundTripHelp":"从本页发起请求到收到结果并完成界面刷新的总耗时,含网络往返与浏览器渲染。","lab.metric.backendTotalMs":"后端总用时","lab.metric.openDecodeMs":"读取/解码","lab.metric.preprocessMs":"预处理","lab.metric.extractMs":"提星","lab.metric.solveOnlyMs":"匹配解算","lab.metric.probHelp":"Tetra3 的 Prob 为假阳性概率(越低越可信)。一般使用 (1−Prob)×100%;当 Prob 极小时浮点数会舍入为 100%,此时用 −log₁₀(Prob) 映射到约 70–100% 以便区分。","lab.metric.probRawHelp":"Tetra3 返回的原始 Prob 字段,可能为对数似然等内部量;与上一行换算后的百分比对照查看即可。","lab.metric.radec":"RA / Dec","lab.metric.matches":"匹配","lab.metric.rmse":"RMSE","lab.metric.prob":"置信","lab.metric.status":"状态","lab.gateNextMs":"约 {ms} ms 后可重试","lab.gate.skipRecording":"录制中:相机正写入录像,暂无法从相机取帧解算;请先停止录制后再试。","lab.gate.skipBusyInFlight":"上一帧解算仍在处理中,请稍候。若频繁出现,可适当增大「实时解算间隔」。","lab.gate.skipInterval":"解算请求过于频繁,已按后端节流排队。","lab.gate.skipBusyFallback":"实时解算暂时无法开始({detail})。","lab.centroidQualityTitle":"质心质量","lab.centroidQualityMetrics":"输入 {in} → 保留 {out};剔除过密 {dense},共线 {line}","meta.exposure":"曝光","meta.gain":"增益","meta.fps":"帧率","meta.sensor":"传感器","meta.colorMode":"色彩","meta.outputResolution":"输出分辨率","meta.fileTime":"文件时间","meta.fileSize":"文件大小","results.viewRaw":"原始 JSON","results.hideRaw":"收起","results.solveHistoryTitle":"解算历史","results.historySingle":"单帧","results.historyBatch":"批量 {count} 组","params.title":"解算参数","params.blockSolveIntro":"以下为板块求解(Tetra3)搜索天区、超时与粗略指向提示;FOV 需与镜头视场大致一致。","params.centroid":"提星","params.blockCentroidIntro":"以下为星点检测:阈值、连通域面积与局部背景窗口,用于从图像中提取星点质心。","params.fov":"FOV 估计 (°)","params.fovHelp":"水平视场角估计值,用于 lost-in-space 解算。","params.fovErr":"FOV 允许误差 (°)","params.fovErrHelp":"允许 Tetra3 在估计 FOV 附近的搜索范围。","params.timeout":"超时 (ms)","params.timeoutHelp":"单次解算最长等待时间。","params.solveIntervalMs":"实时解算间隔 (ms)","params.solveIntervalMsHelp":"期望间隔可调整,但会被后端限制在安全范围内。","params.solveIntervalIndependent":"解算间隔独立于相机采集和预览帧率。","params.solveIntervalBound":"后端限制范围:{min}-{max} ms,当前生效:{effective} ms","params.centroidRejectionLevel":"质心剔除强度 (1–5)","params.centroidRejectionLevelHelp":"1 最保守保留更多星点,5 更激进剔除过密与共线假星;默认 3。","params.centroidRejectionScale":"保守 ← → 激进","params.solveProfile":"解算档位","params.solveProfileHelp":"速度/平衡/稳健三档会同时调整超时、提星阈值与参与匹配星点数。","params.solveProfileSpeed":"速度优先","params.solveProfileBalanced":"平衡","params.solveProfileRobust":"稳健优先","params.ra":"RA 提示 (°)","params.raHelp":"大致天球赤经,缩小搜索范围(度)。","params.dec":"Dec 提示 (°)","params.decHelp":"大致天球赤纬(度)。","params.maxSide":"提星前长边上界 (px)","params.maxSideHelp":"降采样长边上限,大图可加速提星。","params.detailLevelFull":"包含完整 Tetra3 原始结果(体积略大,仅调试时开启)","params.largeScaleBg":"大尺度背景减除","params.largeScaleBgHelp":"在提星前用低分辨率平滑估计并校正大尺度亮度不均,可减轻角部光晕导致的假星;默认关闭以保持与过往行为一致。","params.sigma":"σ(阈值倍数)","params.sigmaHelp":"高于背景噪声倍数的区域视为星点候选。","params.maxArea":"max_area","params.maxAreaHelp":"连通域最大像素面积。","params.minArea":"min_area","params.minAreaHelp":"连通域最小像素面积。","params.filtsize":"filtsize(奇数)","params.filtsizeHelp":"局部背景滤波窗口边长,须为奇数。","btn.solveOne":"单张解算","btn.solveBatch":"批量解算(勾选预设)","btn.applyPresets":"应用预设到表单","btn.savePreset":"保存","placeholder.newPreset":"新预设名称","pool.title":"服务器素材池","pool.col.name":"文件名","pool.col.source":"来源","pool.col.size":"大小","pool.col.time":"修改时间","pool.delete":"删除","history.title":"实验记录","history.intro":"此处展示你在解算台完成解算后手动保存的快照。用法:在「解算台」主栏「结果对比」中,单张解算后点「保存当前到实验记录」,或批量解算后在某张结果卡片上点「保存记录」。本页可按文件名或预设名搜索,支持导出 JSON/CSV 备份。","history.search":"搜索…","history.searchBtn":"搜索","history.exportJson":"导出 JSON","history.exportCsv":"导出 CSV","history.total":"共 {n} 条","history.preset":"预设","history.metrics":"指标","history.detail":"详情","history.collapse":"收起","history.prev":"上一页","history.next":"下一页","history.delete":"删除","delete.uploadFirst":"确定要从素材池删除「{name}」吗?","delete.uploadSecond":"此操作不可恢复,再次确认删除?","delete.experimentFirst":"确定要删除这条实验记录吗?","delete.experimentSecond":"此操作不可恢复,再次确认删除?","results.title":"结果对比","results.saveCurrent":"保存当前到实验记录","results.saveRow":"保存记录","results.expand":"展开","results.collapseJson":"收起","err.selectFile":"请选择素材","err.selectPresets":"请勾选至少一个预设","common.placeholder":"—","delete.uploadCascade":"该素材有 {n} 条实验记录,是否一并删除?","lab.solveCurrentFrame":"解算当前帧(文件)","lab.solveFileStart":"开始文件连续解算","lab.solveFileStop":"停止文件连续解算","lab.cameraPreviewLoading":"正在连接共享预览…","lab.solveCameraFrame":"解算相机当前帧","lab.solveCameraStart":"开始相机解算","lab.solveCameraStop":"停止相机解算","lab.videoPreviewFailed":"该视频格式可能不受浏览器支持,建议使用 MP4(H.264) 或 WebM。","lab.previewModeFile":"素材文件","lab.previewModeCamera":"设备相机","lab.stopCameraPreview":"停止预览","lab.videoLiveIntro":"与调试控制台共用同一相机;此处可预览并解算实时帧,无需单独打开调试页。两页可同时使用。","lab.videoMjpegHint":"MJPEG 同时连接数已满:请关闭其它页面的实时预览或稍后重试(默认允许约 4 路,可在 systemd 调整 OGSCOPE_STREAM_MAX_MJPEG_CLIENTS)。","lab.cameraStreamStatusFail":"无法读取视频流名额状态,请稍后重试。","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"原始 Prob","lab.systemLoad":"系统负载","results.saveBatchAll":"保存全部到实验记录","lab.transcode.title":"AVI 文件需先转码","lab.transcode.desc":"当前视频为 AVI。为保证浏览器可预览与连续解算,请先在本地转码为 MP4 并上传替换。上传成功后将自动删除服务器上的原 AVI。","lab.transcode.button":"转码并上传替换","lab.transcode.loading":"正在加载转码器…","lab.transcode.writingBuffer":"正在写入转码缓冲区…","lab.transcode.running":"正在转码 AVI -> MP4…","lab.transcode.packaging":"正在封装转码结果…","lab.transcode.uploading":"正在上传转码结果…","lab.transcode.done":"转码并替换完成,可继续预览与解算。","lab.transcode.failed":"转码或上传失败,请重试。","lab.transcode.fetchFailed":"无法读取服务器上的 AVI 文件。","sys.shell.subtitle":"系统调试控制台","sys.shell.nav.overview":"总览","sys.shell.nav.network":"网络","sys.shell.nav.camera":"相机调试","sys.shell.nav.analysis":"寻星控制台","sys.shell.nav.sensors":"传感器","sys.shell.nav.power":"电源","sys.shell.nav.hmi":"人机交互","sys.shell.nav.config":"配置管理","sys.shell.workbench":"WORKBENCH / 系统工作台","sys.shell.node":"节点","sys.shell.top.overview":"系统状态","sys.shell.top.network":"网络与 WiFi","sys.shell.top.sensors":"传感器","sys.shell.top.power":"电源管理","sys.shell.top.hmi":"人机交互","sys.shell.top.config":"配置管理","sys.overview.breadcrumb.console":"控制台","sys.overview.breadcrumb.module":"系统总览","sys.overview.title":"系统总览","sys.overview.subtitle":"实时系统运行健康状况","sys.overview.metric.cpu":"CPU 使用率","sys.overview.metric.mem":"内存占用","sys.overview.metric.temp":"核心温度","sys.overview.metric.wifi":"WiFi 链路","sys.overview.metric.uptime":"运行时长","sys.overview.metric.load":"1 分钟负载","sys.overview.metric.storage":"存储","sys.overview.linkActive":"链路在线","sys.overview.tempState":"状态: 正常","sys.overview.wifiSummary":"无线网络摘要","sys.overview.iface":"接口","sys.overview.signal":"信号","sys.overview.quality":"质量","sys.overview.storageComingSoon":"即将接入","sys.logs.title":"系统日志","sys.logs.kernel":"内核","sys.logs.liveToggle":"实时拉取","sys.logs.refresh":"刷新","sys.logs.liveOffHint":"实时拉取关闭(默认)","sys.logs.loading":"日志加载中...","sys.logs.empty":"暂无日志","sys.placeholder.breadcrumb":"控制台 / 预留模块","sys.placeholder.block":"预留模块","sys.placeholder.desc":"模块壳层已预留,后续将按统一组件体系接入真实数据。","sys.placeholder.status":"STATUS: RESERVED / 当前为结构化占位页面。","sys.placeholder.sensors.title":"传感器诊断台","sys.placeholder.sensors.desc":"用于 IMU、温湿度、电流电压、姿态与时序采样的统一监控面板。","sys.placeholder.sensors.block1":"实时曲线区","sys.placeholder.sensors.block2":"设备健康卡","sys.placeholder.sensors.block3":"阈值告警区","sys.sensors.title":"传感器诊断台","sys.sensors.desc":"磁力计罗盘指北与 MPU-6050 陀螺仪角速度可视化;底层仍为开发者调试 API。","sys.sensors.mag.section":"磁力计(AK09911 / I²C)","sys.sensors.mag.note":"默认总线 1、地址 12(0x0C,CAD 接 GND)。无需在应用层“定义 GPIO”;请确保固件已启用 I²C 且已加载 i2c-dev。","sys.sensors.mag.bus":"I²C 总线号","sys.sensors.mag.addr":"7-bit 地址(十进制,12=0x0C)","sys.sensors.mag.i2cdetect":"附带运行 i2cdetect","sys.sensors.mag.btnSelftest":"运行自检","sys.sensors.mag.btnProbe":"扫描各总线","sys.sensors.mag.btnCalStart":"开始方向校准","sys.sensors.mag.btnCalCommit":"保存并锁定方向","sys.sensors.mag.btnCalReset":"重置到自动模式","sys.sensors.mag.btnCalStatus":"查看校准状态","sys.sensors.mag.running":"执行中…","sys.sensors.mpu.section":"IMU(MPU-6050 / I²C)","sys.sensors.mpu.note":"默认总线 1、地址 104(0x68,AD0 接 GND)。含陀螺仪角速度采样(MPU 内集成加速度计与陀螺仪)。","sys.sensors.mpu.addr":"7-bit 地址(十进制,104=0x68)","sys.sensors.mpu.btnSelftest":"运行 MPU 自检","sys.sensors.mpu.running":"执行中…","sys.sensors.gyro.title":"陀螺仪(MPU-6050)","sys.sensors.gyro.subtitle":"使用上方同一总线/地址;一次读取加速度与陀螺仪,估算横滚/俯仰并显示 3D 板卡示意。","sys.sensors.gyro.btn":"读取角速度","sys.sensors.gyro.loading":"采样中…","sys.sensors.gyro.dps":"角速度","sys.sensors.gyro.raw":"原始值","sys.sensors.gyro.unitDps":"°/s,±250°/s 量程、默认灵敏度","sys.sensors.gyro.unitRaw":"16-bit 有符号","sys.sensors.gyro.hint":"点击「读取角速度」从 IMU 采样(加速度 0x3B + 陀螺仪 0x43)。横滚/俯仰来自重力;ωz 为绕竖直轴角速度,并非绝对航向。","sys.sensors.gyro.errUnknown":"采样失败","sys.sensors.gyro.live":"连续刷新(约 2 Hz)","sys.sensors.gyro.barsTitle":"角速度条(相对 ±250°/s 满偏)","sys.sensors.gyro.rawBlock":"寄存器原始值(16-bit)","sys.sensors.gyro.att3dTitle":"3D 姿态示意","sys.sensors.gyro.att3dDesc":"横滚/俯仰由加速度计重力估算;ωz 为陀螺仪绕竖直轴角速度(非磁航向)。机体系三轴与板卡固连,随姿态在三维空间中转动。","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"左侧为与电路板固连的机体系:琥珀 +X、天蓝 +Y、紫 +Z(法向);短划为负向参考,整体随横滚/俯仰旋转。","sys.sensors.gyro.att3dRefTitle":"正交轴参考(固定视角)","sys.sensors.gyro.att3dRefDesc":"与左侧同色对应,便于辨认三轴空间关系。","sys.sensors.gyro.roll":"横滚","sys.sensors.gyro.pitch":"俯仰","sys.sensors.gyro.yawRate":"偏航角速度","sys.sensors.compass.tapeCaption":"水平航向带 · 中心为当前读数","sys.sensors.compass.title":"磁力计 · 指北罗盘","sys.sensors.compass.desc":"根据水平面磁场分量估算方向角,用于调试「是否大致指向北方」;精对准需校准与固定安装姿态。","sys.sensors.compass.btn":"刷新罗盘","sys.sensors.compass.loading":"测量中…","sys.sensors.compass.live":"连续刷新","sys.sensors.compass.headingLabel":"水平航向角","sys.sensors.compass.headingHint":"由 atan2(Hx, Hy) 得到,单位度","sys.sensors.compass.cardN":"北","sys.sensors.compass.cardE":"东","sys.sensors.compass.cardS":"南","sys.sensors.compass.cardW":"西","sys.sensors.compass.err":"采样失败","sys.sensors.compass.footnote":"红色针尖表示 XY 平面内磁场水平分量方向;转动设备时指针应随之变化。精对准需软铁/硬铁校准。","sys.sensors.jsonToggle":"展开原始 JSON(排障用)","sys.hmi.title":"人机交互 · SPI 显示","sys.hmi.desc":"通过硬件平面驱动 ST7796(默认 320×320);需 OGSCOPE_DISPLAY_ENABLED=true 且系统已启用 SPI(/dev/spidev0.0)。","sys.hmi.status.section":"显示与硬件平面状态","sys.hmi.status.refresh":"刷新状态","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"存在","sys.hmi.status.no":"不存在","sys.hmi.status.resolution":"分辨率 / DC","sys.hmi.status.driver":"驱动句柄","sys.hmi.status.open":"已打开","sys.hmi.status.closed":"未打开","sys.hmi.status.screenOutput":"逻辑输出","sys.hmi.status.on":"开","sys.hmi.status.off":"关","sys.hmi.status.lastPattern":"上次图案","sys.hmi.status.lastError":"上次错误","sys.hmi.actions.section":"输出到屏幕","sys.hmi.actions.hint":"以下为硬件平面 device.command → hmi;大屏刷新使用较长 RPC 超时。","sys.hmi.actions.smoke":"测试画面(文字+边框)","sys.hmi.actions.colorbars":"彩条","sys.hmi.actions.fill":"填充 RGB","sys.hmi.actions.screenOn":"允许屏幕输出","sys.hmi.actions.screenOff":"暂停屏幕输出","sys.hmi.actions.release":"释放驱动(关闭 SPI/GPIO)","sys.hmi.rawJson":"原始 JSON 响应","sys.placeholder.hmi.title":"人机交互台","sys.placeholder.hmi.desc":"用于 SPI 显示、按键矩阵、背光/对比度与界面回放的调试工作区。","sys.placeholder.hmi.block1":"显示预览区","sys.placeholder.hmi.block2":"输入事件流","sys.placeholder.hmi.block3":"参数与模式控制","sys.placeholder.power.title":"电源与功耗台","sys.placeholder.power.desc":"用于电池、充电、瞬时功耗与热管理策略联动调试。","sys.placeholder.power.block1":"功耗看板","sys.placeholder.power.block2":"电源策略表","sys.placeholder.power.block3":"恢复与保护动作","cam.title":"相机调试控制台","cam.subtitle":"迁移到统一后台架构,保留关键性能优化","cam.btn.system":"系统后台","cam.btn.analysis":"寻星控制台","cam.btn.home":"首页","cam.btn.start":"启动预览","cam.btn.starting":"启动中...","cam.btn.stop":"停止预览","cam.btn.stopping":"停止中...","cam.btn.capture":"拍摄","cam.btn.capturing":"拍摄中...","cam.btn.recordStart":"开始录制","cam.btn.recordStop":"停止录制","cam.btn.recordBusy":"录制切换中...","cam.btn.refresh":"刷新状态","cam.preview.title":"实时预览","cam.preview.state":"状态","cam.preview.mode":"曝光模式","cam.preview.emptyTitle":"预览尚未启动","cam.preview.emptyDesc":"点击下方按钮启动相机预览流。","cam.state.streaming":"流已启动","cam.state.idle":"未启动","cam.state.rec":"录制中","cam.stats.requestFps":"请求 FPS","cam.hint.mjpegSingleStream":"MJPEG 同时连接数有限(默认约两路);超出上限时多标签页会争用或返回忙,请减少同时打开的实时预览。","cam.stats.frameFps":"画面 FPS","cam.stats.fpsMeasureNote":"统计最近约 1 秒内在 32×32 采样上的像素变化次数;多路同时预览、网络较慢、或下方「共享流上限」低于相机目标时,读数常明显偏低。","cam.err.streamBusy":"无法连接预览:MJPEG 名额已被占用","cam.err.streamBusyHint":"请关闭其它标签页或解算台中的设备实时预览,或稍后再试。","cam.err.streamProbeFailed":"无法建立预览流","cam.err.streamProbeDetail":"请检查网络与相机服务状态,或稍后重试。","cam.stats.targetFps":"相机采集目标 FPS","cam.stats.streamPacingFps":"共享流/MJPEG 上限 FPS","cam.stats.streamPacingHint":"与进程环境 OGSCOPE_SHARED_PREVIEW_FPS 一致,约束共享抓帧节奏与每路 MJPEG 的最小帧间隔。","cam.stats.captureFps":"实际采集 FPS","cam.stats.encodeMs":"JPEG 平均编码","cam.stats.encoder":"预览编码器","cam.stats.driver":"相机驱动","cam.stats.metadata":"元数据","cam.stats.consumers":"预览/分析/录像消费者","cam.stats.cameraMemory":"进程内存/Swap","cam.stats.longExposureThrottle":"自动曝光约 {exposure} ms,实际帧率受长曝光限制;为保证分析效果未强制缩短曝光。","cam.stats.uptime":"流运行时长","cam.controls.title":"参数控制","cam.controls.core":"核心参数","cam.controls.fps":"帧率","cam.controls.sensorFps":"传感器目标 FPS","cam.controls.previewFps":"预览目标 FPS","cam.controls.resolution":"分辨率","cam.controls.applyRes":"应用分辨率","cam.controls.sampling":"采样模式","cam.controls.exposure":"曝光","cam.controls.gain":"模拟增益","cam.controls.digitalGain":"数字增益","cam.controls.noiseReduction":"降噪级别","cam.controls.noiseReductionMode":"降噪模式","cam.controls.digitalGainReadOnly":"当前驱动仅上报数字增益,不支持手动设置","cam.controls.aeFlicker":"AE 防闪烁","cam.controls.maxAeFrame":"最长自动曝光帧周期(us)","cam.controls.contrast":"对比度","cam.controls.brightness":"亮度","cam.controls.saturation":"饱和度","cam.controls.sharpness":"锐度","cam.controls.applySettings":"应用全部设置","cam.controls.applyAll":"保存参数","cam.controls.applyRuntime":"保存运行参数","cam.controls.mode":"模式控制","cam.controls.autoExposure":"曝光模式","cam.controls.whiteBalance":"白平衡","cam.controls.colorMode":"颜色模式","cam.controls.auto":"自动","cam.controls.manual":"手动","cam.controls.night":"夜间","cam.controls.off":"关闭","cam.controls.nr.off":"关闭","cam.controls.nr.fast":"快速","cam.controls.nr.high_quality":"高质量","cam.controls.wb.auto":"自动","cam.controls.wb.daylight":"日光","cam.controls.wb.cloudy":"阴天","cam.controls.wb.tungsten":"钨丝灯","cam.controls.wb.fluorescent":"荧光灯","cam.controls.wb.indoor":"室内","cam.controls.wb.manual":"手动","cam.controls.wb.night":"夜间","cam.controls.color":"彩色","cam.controls.mono":"黑白","cam.controls.applyAe":"应用曝光模式","cam.controls.applyWb":"应用白平衡","cam.controls.applyColor":"应用颜色模式","cam.controls.applyMode":"应用模式设置","cam.controls.lockedByAe":"自动曝光开启时,曝光与增益参数锁定。","cam.controls.lockedByWb":"当前非手动白平衡模式,R/B 增益参数锁定。","cam.controls.pendingChanges":"有未应用的参数修改","cam.controls.tools":"工具","cam.controls.nightPreset":"夜间预设","cam.controls.nightOn":"开启夜间模式","cam.controls.nightOff":"关闭夜间模式","cam.controls.backup":"备份设置","cam.controls.restore":"恢复设置","cam.controls.reset":"重置相机","cam.quick.title":"快速预设与智能调参","cam.quick.daylight":"白天模式","cam.quick.night":"夜间模式","cam.quick.nightPreset":"应用夜间预设","cam.quick.deep-sky":"深空模式","cam.quick.planetary":"行星模式","cam.quick.autoAdjust":"智能调整","cam.quick.nightHint":"夜间预设用于一键切换夜拍参数;夜间模式开关用于实时启停红外/夜景模式。","cam.system.title":"系统监控","cam.system.status":"相机就绪","cam.system.stream":"流状态","cam.system.sensor":"传感器","cam.system.quality":"图像质量","cam.presets.title":"预设管理","cam.presets.name":"预设名称","cam.presets.desc":"预设描述","cam.presets.save":"保存预设","cam.presets.empty":"暂无预设","cam.presets.noDesc":"无描述","cam.presets.apply":"应用","cam.presets.delete":"删除","cam.files.title":"文件管理","cam.files.refresh":"刷新文件列表","cam.files.empty":"暂无文件","cam.files.download":"下载","cam.files.info":"详情","cam.files.delete":"删除","cam.files.loadingInfo":"正在读取文件详情...","cam.files.prev":"上一页","cam.files.next":"下一页","cam.files.page":"第 {current}/{total} 页","cam.files.closeDetail":"关闭详情","cam.files.size":"文件大小","cam.files.type":"文件类型","cam.files.modified":"修改时间","cam.hist.title":"直方图","cam.hist.enabled":"启用直方图","cam.hist.luminance":"亮度通道","cam.hist.over":"过曝警告","cam.hist.expand":"展开直方图","cam.hist.collapse":"收起直方图","cam.notice.previewStart":"预览已启动","cam.notice.previewStop":"预览已停止","cam.notice.captureSaved":"拍摄完成: {name}","cam.notice.recordStart":"开始录制: {name}","cam.notice.recordStop":"录制已停止","cam.notice.fpsApplied":"帧率已设置为 {fps}","cam.notice.resApplied":"分辨率已设置为 {w}x{h}","cam.notice.samplingApplied":"采样模式已设置为 {mode}","cam.notice.settingsApplied":"参数已应用","cam.notice.aeApplied":"曝光模式已应用","cam.notice.wbApplied":"白平衡已应用","cam.notice.colorApplied":"颜色模式已应用","cam.notice.modeApplied":"模式设置已应用","cam.notice.runtimeApplied":"运行参数已应用","cam.notice.rotationApplied":"旋转已设置为 {value}°","cam.mirror.hint":"相机倒装时可配合旋转使用;镜像后预览与星点解算、极轴引导同坐标系。","cam.mirror.horizontal":"水平镜像","cam.mirror.vertical":"垂直镜像","cam.notice.mirrorApplied":"镜像设置已更新","cam.notice.nightOn":"夜间模式已开启","cam.notice.nightOff":"夜间模式已关闭","cam.notice.nightPreset":"夜间预设已应用","cam.notice.reset":"相机设置已重置","cam.notice.backup":"设置已备份","cam.notice.restore":"设置已恢复","cam.notice.presetSaved":"预设已保存: {name}","cam.notice.presetApplied":"预设已应用: {name}","cam.notice.presetDeleted":"预设已删除: {name}","cam.notice.download":"已开始下载: {name}","cam.notice.downloadWithSidecar":"已下载: {name}(含侧车 {sidecar})","cam.notice.fileDeleted":"文件已删除: {name}","cam.notice.quickPresetApplied":"已应用快速预设: {preset}","cam.notice.needPreview":"请先启动预览","cam.notice.needManualForAutoAdjust":"请先切换为手动曝光再进行智能调整","cam.notice.autoAdjusted":"智能调整已完成并生效","cam.confirm.deletePreset":"确定删除预设 {name} 吗?","cam.confirm.deleteFile":"确定删除文件 {name} 吗?","sys.sensors.mag.calStatusPrefix":"校准状态:","sys.sensors.mag.calModeAuto":"自动模式(未锁定)","sys.sensors.mag.calModeRecording":"录制中(请旋转设备)","sys.sensors.mag.calModeLocked":"已锁定(使用保存方向)","sys.sensors.mag.calSamplesPrefix":"录制样本:","sys.sensors.mag.calRecordingHint":"操作提示:保持水平,缓慢旋转设备,建议累计 10+ 样本后保存。","sys.sensors.mag.calLockedHint":"已锁定方向参数:"},g=a.createContext(null),p={zh:C,en:k},b="ogscope.analysis.locale";function y(){const e=window.localStorage.getItem(b);return e==="zh"||e==="en"?e:(navigator.language||"zh").toLowerCase().startsWith("en")?"en":"zh"}function R({children:e}){const[s,t]=a.useState(y),[o,l]=a.useState(p[y()]);a.useEffect(()=>{l(p[s]),window.localStorage.setItem(b,s),document.documentElement.lang=s==="en"?"en":"zh-CN"},[s]);const r=a.useMemo(()=>(n,c)=>{let i=o[n]??n;if(c)for(const[d,h]of Object.entries(c))i=i.replace(new RegExp(`\\{${d}\\}`,"g"),String(h));return i},[o]),m=a.useMemo(()=>({locale:s,setLocale:t,t:r}),[s,r]);return v.jsx(g.Provider,{value:m,children:e})}function x(){const e=a.useContext(g);if(!e)throw new Error("useI18n must be used within I18nProvider");return e}export{R as I,A as T,P as c,x as u}; diff --git a/web/static/analysis-lab/assets/index-CV2LaaT8.js b/web/static/analysis-lab/assets/index-CV2LaaT8.js new file mode 100644 index 0000000..75a83a2 --- /dev/null +++ b/web/static/analysis-lab/assets/index-CV2LaaT8.js @@ -0,0 +1,26 @@ +import{r as a,j as v}from"./client-D1ZVDB-N.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 f=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),u=(...e)=>e.filter((s,t,o)=>!!s&&s.trim()!==""&&o.indexOf(s)===t).join(" ").trim();/** + * @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. + */var w={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @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 S=a.forwardRef(({color:e="currentColor",size:s=24,strokeWidth:t=2,absoluteStrokeWidth:o,className:l="",children:r,iconNode:m,...n},c)=>a.createElement("svg",{ref:c,...w,width:s,height:s,stroke:e,strokeWidth:o?Number(t)*24/Number(s):t,className:u("lucide",l),...n},[...m.map(([i,d])=>a.createElement(i,d)),...Array.isArray(r)?r:[r]]));/** + * @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 P=(e,s)=>{const t=a.forwardRef(({className:o,...l},r)=>a.createElement(S,{ref:r,iconNode:s,className:u(`lucide-${f(e)}`,o),...l}));return t.displayName=`${e}`,t};/** + * @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 A=P("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]),k={"app.title":"OGScope Plate Solve Console","nav.lab":"Lab","nav.labImage":"Image solve","nav.labVideo":"Video solve","delete.uploadCascade":"Delete {n} linked experiment record(s) as well?","lab.solveCurrentFrame":"Solve current frame (file)","lab.solveFileStart":"Start continuous file solve","lab.solveFileStop":"Stop continuous file solve","lab.cameraPreviewLoading":"Connecting to shared preview…","lab.solveCameraFrame":"Solve live camera frame","lab.solveCameraStart":"Start camera solve","lab.solveCameraStop":"Stop camera solve","lab.videoPreviewFailed":"This video format may be unsupported by the browser. Try MP4 (H.264) or WebM.","lab.previewModeFile":"Pool file","lab.previewModeCamera":"Device camera","lab.videoLiveIntro":"Shares the same camera as Camera Debug — preview and solve live frames here without opening debug. Both pages can run together.","lab.videoMjpegHint":"MJPEG slots are full: close live previews on other pages or retry later (default ~4; tune OGSCOPE_STREAM_MAX_MJPEG_CLIENTS in systemd).","lab.cameraStreamStatusFail":"Could not read MJPEG slot status. Please retry shortly.","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"Raw Prob","lab.systemLoad":"System load","results.saveBatchAll":"Save all to records","nav.pool":"Assets","nav.history":"Records","nav.cameraDebug":"Camera Debug","nav.home":"Home","nav.systemAdmin":"Back to System Admin","lang.zh":"中文","lang.en":"EN","sidebar.assets":"Uploaded assets","sidebar.upload":"Upload","sidebar.refresh":"Refresh","sidebar.debugCaptures":"Debug console media","sidebar.assetTypeImage":"Image","sidebar.assetTypeVideo":"Video","sidebar.debugEmpty":"No debug files","sidebar.importToPool":"Import to pool","sidebar.importToPoolWithTranscode":"Transcode & Import","sidebar.importToPoolDirect":"Import Directly","sidebar.flowPreparing":"Preparing...","sidebar.flowImportingDebug":"Importing debug capture...","sidebar.flowDownloadingPool":"Downloading imported file...","sidebar.flowWritingBuffer":"Writing to transcoder buffer...","sidebar.flowLoadingTranscoder":"Loading transcoder...","sidebar.flowTranscoding":"Transcoding AVI -> MP4...","sidebar.flowPackaging":"Packaging output...","sidebar.flowUploadingMp4":"Uploading MP4...","sidebar.flowReplacing":"Replacing and removing original AVI...","sidebar.flowNoTranscode":"No transcode required, imported directly.","sidebar.flowDone":"Completed","sidebar.flowDoneMsg":"Done: {name}","sidebar.flowFailed":"Failed","sidebar.debugPage":"Page {cur} / {total}","sidebar.batchPresets":"Batch presets","sidebar.batchHint":"Check presets, then use Batch solve to compare multiple param sets.","lab.selectOrUpload":"Pick an uploaded or imported asset from the left","lab.selectOrUploadVideo":"Pick a pool video to preview, or use the button above for the live camera frame.","lab.file":"File","lab.source":"Source","lab.layers":"Layers","lab.layer.matched":"Matched","lab.layer.pattern":"Pattern","lab.layer.all":"All centroids","lab.layer.rejected":"Rejected (red cross)","lab.previewConfidence":"Confidence","lab.grid":"Grid","lab.zoomIn":"Zoom in","lab.zoomOut":"Zoom out","lab.zoomReset":"Reset","lab.resolution":"Resolution","lab.fwhm":"FWHM","lab.starsDetected":"Stars detected","lab.meta.title":"Capture & file info","lab.meta.noSidecar":"No sidecar (not from debug capture)","lab.meta.partial":"No detailed sidecar; file info only.","lab.solveSection":"Solve","lab.imageSection":"Image","lab.metric.solveMs":"Time","lab.metric.solveComputeMs":"Solve compute","lab.metric.solveComputeHelp":"Server-side Tetra3 + star extraction only (no network).","lab.metric.solveRoundTripMs":"End-to-end","lab.metric.solveRoundTripHelp":"From request start to UI updated: network + JSON + render.","lab.metric.backendTotalMs":"Backend total","lab.metric.openDecodeMs":"Open/decode","lab.metric.preprocessMs":"Preprocess","lab.metric.extractMs":"Extract","lab.metric.solveOnlyMs":"Solve match","lab.metric.probHelp":"Tetra3 `Prob` is false-positive probability (lower is better). We use (1−Prob)×100% when representable; for extremely small Prob, IEEE doubles round to 100%, so we map −log10(Prob) to ~70–100% for differentiation.","lab.metric.probRawHelp":"Raw Tetra3 Prob (e.g. log-likelihood); compare with the normalized line above.","lab.metric.radec":"RA / Dec","lab.metric.matches":"Matches","lab.metric.rmse":"RMSE","lab.metric.prob":"Prob.","lab.metric.status":"Status","lab.gateNextMs":"Retry in ~{ms} ms","lab.gate.skipRecording":"Recording is active: the camera is writing video, so live-frame solve is paused. Stop recording and try again.","lab.gate.skipBusyInFlight":"The previous solve is still running; please wait. If this repeats often, increase the realtime solve interval.","lab.gate.skipInterval":"Solve requests are arriving faster than allowed; the server is throttling. Wait for the countdown and retry.","lab.gate.skipBusyFallback":"Realtime solve could not start ({detail}).","lab.centroidQualityTitle":"Centroid quality","lab.centroidQualityMetrics":"In {in} → kept {out}; removed dense {dense}, collinear {line}","meta.exposure":"Exposure","meta.gain":"Gain","meta.fps":"FPS","meta.sensor":"Sensor","meta.colorMode":"Color","meta.outputResolution":"Output size","meta.fileTime":"File time","meta.fileSize":"File size","results.viewRaw":"Raw JSON","results.hideRaw":"Hide","results.solveHistoryTitle":"Solve history","results.historySingle":"Single frame","results.historyBatch":"Batch ({count} runs)","params.title":"Solve parameters","params.blockSolveIntro":"Plate-solve (Tetra3): FOV, timeout, and coarse sky hints. FOV should match your lens.","params.centroid":"Star detection","params.blockCentroidIntro":"Star detection: threshold, blob area, and local background window for centroids.","params.fov":"FOV estimate (°)","params.fovHelp":"Horizontal field of view for lost-in-space solve.","params.fovErr":"FOV max error (°)","params.fovErrHelp":"Search range around estimated FOV.","params.timeout":"Timeout (ms)","params.timeoutHelp":"Max wait time per solve.","params.solveIntervalMs":"Realtime solve interval (ms)","params.solveIntervalMsHelp":"Desired interval is adjustable, but backend clamps it into a safe range.","params.solveIntervalIndependent":"Solve cadence is independent from sensor and preview frame rates.","params.solveIntervalBound":"Backend bounds: {min}-{max} ms, effective now: {effective} ms","params.centroidRejectionLevel":"Centroid rejection (1–5)","params.centroidRejectionLevelHelp":"1 keeps more stars; 5 aggressively rejects dense clusters and collinear false detections; default 3.","params.centroidRejectionScale":"Conservative ← → Aggressive","params.solveProfile":"Solve profile","params.solveProfileHelp":"Speed/Balanced/Robust tune timeout, centroid thresholds, and matching star count together.","params.solveProfileSpeed":"Speed first","params.solveProfileBalanced":"Balanced","params.solveProfileRobust":"Robust first","params.ra":"RA hint (°)","params.raHelp":"Approximate right ascension in degrees.","params.dec":"Dec hint (°)","params.decHelp":"Approximate declination in degrees.","params.maxSide":"Max long side before extract (px)","params.maxSideHelp":"Downscale long edge for faster centroid extraction.","params.detailLevelFull":"Include full Tetra3 raw block (larger payload, for debugging only).","params.largeScaleBg":"Large-scale background flattening","params.largeScaleBgHelp":"Before centroiding, estimate a low-frequency background on a downscaled image and correct uneven illumination (e.g. corner glow). Off by default to match legacy behavior.","params.sigma":"σ threshold","params.sigmaHelp":"Multiplier over background noise for star candidates.","params.maxArea":"max_area","params.maxAreaHelp":"Max connected component area in pixels.","params.minArea":"min_area","params.minAreaHelp":"Min connected component area in pixels.","params.filtsize":"filtsize (odd)","params.filtsizeHelp":"Local filter window size, must be odd.","btn.solveOne":"Solve once","btn.solveBatch":"Batch solve (presets)","btn.applyPresets":"Apply preset to form","btn.savePreset":"Save","placeholder.newPreset":"New preset name","pool.title":"Server asset pool","pool.col.name":"Filename","pool.col.source":"Source","pool.col.size":"Size","pool.col.time":"Modified","pool.delete":"Delete","history.title":"Experiment records","history.intro":"Saved solve snapshots from the Lab. After a solve, use Save to records in the Lab main panel (Result comparison), or Save on each batch result card. Search by filename or preset; export JSON/CSV for backup.","history.search":"Search…","history.searchBtn":"Search","history.exportJson":"Export JSON","history.exportCsv":"Export CSV","history.total":"Total {n}","history.preset":"Preset","history.metrics":"Metrics","history.detail":"Details","history.collapse":"Collapse","history.prev":"Prev","history.next":"Next","history.delete":"Delete","delete.uploadFirst":'Delete "{name}" from the asset pool?',"delete.uploadSecond":"This cannot be undone. Confirm again?","delete.experimentFirst":"Delete this experiment record?","delete.experimentSecond":"This cannot be undone. Confirm again?","results.title":"Results","results.saveCurrent":"Save to records","results.saveRow":"Save","results.expand":"Expand","results.collapseJson":"Collapse","err.selectFile":"Select a file","err.selectPresets":"Select at least one preset","common.placeholder":"—","lab.transcode.title":"AVI requires transcoding","lab.transcode.desc":"This video is AVI. For browser preview and continuous solving, transcode it locally to MP4 and upload replacement. The original AVI on server will be removed after success.","lab.transcode.button":"Transcode and Upload Replacement","lab.transcode.loading":"Loading transcoder...","lab.transcode.writingBuffer":"Writing to transcoder buffer...","lab.transcode.running":"Transcoding AVI -> MP4...","lab.transcode.packaging":"Packaging transcoded output...","lab.transcode.uploading":"Uploading transcoded output...","lab.transcode.done":"Transcode and replacement completed.","lab.transcode.failed":"Transcode or upload failed. Please retry.","lab.transcode.fetchFailed":"Failed to fetch AVI from server.","sys.shell.subtitle":"system debug console","sys.shell.nav.overview":"Overview","sys.shell.nav.network":"Network","sys.shell.nav.camera":"Camera Debug","sys.shell.nav.analysis":"Analysis Console","sys.shell.nav.sensors":"Sensors","sys.shell.nav.power":"Power","sys.shell.nav.hmi":"HMI","sys.shell.nav.config":"Config","sys.shell.workbench":"WORKBENCH / System workspace","sys.shell.node":"Node","sys.shell.top.overview":"System Status","sys.shell.top.network":"Network & WiFi","sys.shell.top.sensors":"Sensors","sys.shell.top.power":"Power Management","sys.shell.top.hmi":"HMI","sys.shell.top.config":"Config","sys.overview.breadcrumb.console":"Console","sys.overview.breadcrumb.module":"System Overview","sys.overview.title":"System Overview","sys.overview.subtitle":"Live system telemetry snapshot","sys.overview.metric.cpu":"CPU Usage","sys.overview.metric.mem":"Memory Usage","sys.overview.metric.temp":"Core Temperature","sys.overview.metric.wifi":"WiFi Link","sys.overview.metric.uptime":"Uptime","sys.overview.metric.load":"Load (1m)","sys.overview.metric.storage":"Storage","sys.overview.linkActive":"Link Active","sys.overview.tempState":"status: nominal","sys.overview.wifiSummary":"WiFi Summary","sys.overview.iface":"Interface","sys.overview.signal":"Signal","sys.overview.quality":"Quality","sys.overview.storageComingSoon":"Coming soon","sys.logs.title":"System Logs","sys.logs.kernel":"Kernel","sys.logs.liveToggle":"Live pull","sys.logs.refresh":"Refresh","sys.logs.liveOffHint":"Live pull is off (default)","sys.logs.loading":"Loading logs...","sys.logs.empty":"No logs","sys.placeholder.breadcrumb":"Console / Reserved Module","sys.placeholder.block":"PLANNED BLOCK","sys.placeholder.desc":"Shell layout is reserved and ready for real data integration.","sys.placeholder.status":"STATUS: RESERVED / Structured placeholder page.","sys.placeholder.sensors.title":"Sensors Console","sys.placeholder.sensors.desc":"Unified monitor for IMU, environment, power rails, and timeline sampling.","sys.placeholder.sensors.block1":"Realtime chart area","sys.placeholder.sensors.block2":"Device health cards","sys.placeholder.sensors.block3":"Threshold alerts","sys.sensors.title":"Sensors Console","sys.sensors.desc":"Visual compass (magnetometer) and gyro rate gauges; backed by dev debug APIs.","sys.sensors.mag.section":"Magnetometer (AK09911 / I²C)","sys.sensors.mag.note":"Defaults: bus 1, address 12 (0x0C with CAD to GND). No app-level GPIO mapping is required; enable I²C in firmware and load i2c-dev.","sys.sensors.mag.bus":"I²C bus","sys.sensors.mag.addr":"7-bit address (decimal, 12 = 0x0C)","sys.sensors.mag.i2cdetect":"Run i2cdetect","sys.sensors.mag.btnSelftest":"Run self-test","sys.sensors.mag.btnProbe":"Probe all buses","sys.sensors.mag.btnCalStart":"Start heading calibration","sys.sensors.mag.btnCalCommit":"Save & lock heading","sys.sensors.mag.btnCalReset":"Reset to auto mode","sys.sensors.mag.btnCalStatus":"Calibration status","sys.sensors.mag.running":"Running…","sys.sensors.mpu.section":"IMU (MPU-6050 / I²C)","sys.sensors.mpu.note":"Defaults: bus 1, address 104 (0x68 with AD0 low). Includes gyro angular-rate sampling (MPU integrates accel + gyro).","sys.sensors.mpu.addr":"7-bit address (decimal, 104 = 0x68)","sys.sensors.mpu.btnSelftest":"Run MPU self-test","sys.sensors.mpu.running":"Running…","sys.sensors.gyro.title":"Gyroscope (MPU-6050)","sys.sensors.gyro.subtitle":"Uses the same bus/address as above; one IMU read: 3-axis gyro, accel-derived roll/pitch, and a 3D board hint.","sys.sensors.gyro.btn":"Read angular rate","sys.sensors.gyro.loading":"Sampling…","sys.sensors.gyro.dps":"Angular rate","sys.sensors.gyro.raw":"Raw","sys.sensors.gyro.unitDps":"°/s, ±250 °/s range, default sensitivity","sys.sensors.gyro.unitRaw":"signed 16-bit","sys.sensors.gyro.hint":"Click “Read angular rate” to sample accel (0x3B) and gyro (0x43). Roll/pitch use gravity; ωz is yaw rate, not absolute heading.","sys.sensors.gyro.errUnknown":"Sample failed","sys.sensors.gyro.live":"Live update (~2 Hz)","sys.sensors.gyro.barsTitle":"Angular rate bars (±250 °/s full scale)","sys.sensors.gyro.rawBlock":"Raw register values (16-bit)","sys.sensors.gyro.att3dTitle":"3D attitude hint","sys.sensors.gyro.att3dDesc":"Roll/pitch from accelerometer (gravity); ωz is yaw rate (not magnetic heading). Body XYZ triad is fixed to the PCB and rotates in 3D.","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"Body frame on the PCB: +X amber, +Y sky, +Z violet (normal); short dashes mark negative directions; the whole frame follows roll/pitch.","sys.sensors.gyro.att3dRefTitle":"Orthogonal axes (fixed view)","sys.sensors.gyro.att3dRefDesc":"Same colors as left; use this to read XYZ directions at a glance.","sys.sensors.gyro.roll":"Roll","sys.sensors.gyro.pitch":"Pitch","sys.sensors.gyro.yawRate":"Yaw rate","sys.sensors.compass.tapeCaption":"Heading tape · center = current readout","sys.sensors.compass.title":"Magnetometer · North compass","sys.sensors.compass.desc":"Horizontal-plane heading for “roughly north” debugging; precise alignment needs calibration and a fixed mount definition.","sys.sensors.compass.btn":"Refresh compass","sys.sensors.compass.loading":"Measuring…","sys.sensors.compass.live":"Live update","sys.sensors.compass.headingLabel":"Horizontal heading","sys.sensors.compass.headingHint":"From atan2(Hx, Hy), degrees","sys.sensors.compass.cardN":"N","sys.sensors.compass.cardE":"E","sys.sensors.compass.cardS":"S","sys.sensors.compass.cardW":"W","sys.sensors.compass.err":"Sample failed","sys.sensors.compass.footnote":"The red tip tracks the horizontal XY field direction; it should move as you rotate the board. Use soft/hard-iron calibration for tight north lock.","sys.sensors.jsonToggle":"Raw JSON (troubleshooting)","sys.hmi.title":"HMI · SPI display","sys.hmi.desc":"ST7796 via hardware plane (default 320×320). Requires OGSCOPE_DISPLAY_ENABLED=true and SPI enabled (/dev/spidev0.0).","sys.hmi.status.section":"Display & hardware plane","sys.hmi.status.refresh":"Refresh","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"present","sys.hmi.status.no":"missing","sys.hmi.status.resolution":"Resolution / DC","sys.hmi.status.driver":"Driver handle","sys.hmi.status.open":"open","sys.hmi.status.closed":"closed","sys.hmi.status.screenOutput":"Logical output","sys.hmi.status.on":"on","sys.hmi.status.off":"off","sys.hmi.status.lastPattern":"Last pattern","sys.hmi.status.lastError":"Last error","sys.hmi.actions.section":"Output to panel","sys.hmi.actions.hint":"Uses hardware plane device.command → hmi; large frames use a longer RPC timeout.","sys.hmi.actions.smoke":"Test pattern (text + frame)","sys.hmi.actions.colorbars":"Color bars","sys.hmi.actions.fill":"Fill RGB","sys.hmi.actions.screenOn":"Enable panel output","sys.hmi.actions.screenOff":"Pause panel output","sys.hmi.actions.release":"Release driver (close SPI/GPIO)","sys.hmi.rawJson":"Raw JSON response","sys.placeholder.hmi.title":"HMI Console","sys.placeholder.hmi.desc":"Debug workspace for SPI display, key matrix, backlight, and UI replay.","sys.placeholder.hmi.block1":"Display preview","sys.placeholder.hmi.block2":"Input event stream","sys.placeholder.hmi.block3":"Mode controls","sys.placeholder.power.title":"Power Console","sys.placeholder.power.desc":"Debug workspace for battery, charging, power profile, and thermal policy.","sys.placeholder.power.block1":"Power dashboard","sys.placeholder.power.block2":"Power policy table","sys.placeholder.power.block3":"Recovery actions","cam.title":"Camera Debug Console","cam.subtitle":"Migrated to unified admin UI with key optimizations preserved","cam.btn.system":"System Admin","cam.btn.analysis":"Analysis Console","cam.btn.home":"Home","cam.btn.start":"Start Preview","cam.btn.starting":"Starting...","cam.btn.stop":"Stop Preview","cam.btn.stopping":"Stopping...","cam.btn.capture":"Capture","cam.btn.capturing":"Capturing...","cam.btn.recordStart":"Start Recording","cam.btn.recordStop":"Stop Recording","cam.btn.recordBusy":"Switching recording...","cam.btn.refresh":"Refresh Status","cam.preview.title":"Live Preview","cam.preview.state":"State","cam.preview.mode":"Exposure Mode","cam.preview.emptyTitle":"Preview is not started","cam.preview.emptyDesc":"Start the camera preview stream with the button below.","cam.state.streaming":"streaming","cam.state.idle":"idle","cam.state.rec":"REC","cam.stats.requestFps":"Request FPS","cam.hint.mjpegSingleStream":"MJPEG concurrent streams are limited (default ~2). Beyond the cap, extra tabs may contend or get busy—reduce simultaneous live previews.","cam.stats.frameFps":"Display FPS","cam.stats.fpsMeasureNote":"Pixel deltas on a 32×32 sample over ~1s. Multiple previews, slow links, or a stream cap below the sensor target often read much lower.","cam.err.streamBusy":"Preview unavailable: MJPEG slot is already in use","cam.err.streamBusyHint":"Close the live preview in other tabs or in the Analysis console, then try again.","cam.err.streamProbeFailed":"Could not open the preview stream","cam.err.streamProbeDetail":"Check network and camera service, or retry in a moment.","cam.stats.targetFps":"Camera capture target FPS","cam.stats.streamPacingFps":"Shared stream / MJPEG cap FPS","cam.stats.streamPacingHint":"Matches OGSCOPE_SHARED_PREVIEW_FPS: shared grabber pacing and per-stream MJPEG min frame spacing.","cam.stats.captureFps":"Actual capture FPS","cam.stats.encodeMs":"Average JPEG encode","cam.stats.encoder":"Preview encoder","cam.stats.driver":"Camera driver","cam.stats.metadata":"Metadata","cam.stats.consumers":"Preview/analysis/record consumers","cam.stats.cameraMemory":"Process memory/swap","cam.stats.longExposureThrottle":"Auto exposure is about {exposure} ms, so long exposure is limiting FPS; exposure is not shortened to preserve analysis quality.","cam.stats.uptime":"Stream Uptime","cam.controls.title":"Controls","cam.controls.core":"Core Settings","cam.controls.fps":"FPS","cam.controls.sensorFps":"Sensor target FPS","cam.controls.previewFps":"Preview target FPS","cam.controls.resolution":"Resolution","cam.controls.applyRes":"Apply resolution","cam.controls.sampling":"Sampling mode","cam.controls.exposure":"Exposure","cam.controls.gain":"Analog Gain","cam.controls.digitalGain":"Digital Gain","cam.controls.noiseReduction":"Noise Reduction","cam.controls.noiseReductionMode":"Noise Reduction","cam.controls.digitalGainReadOnly":"Digital gain is read-only on this driver","cam.controls.aeFlicker":"AE Flicker","cam.controls.maxAeFrame":"Max AE frame (us)","cam.controls.contrast":"Contrast","cam.controls.brightness":"Brightness","cam.controls.saturation":"Saturation","cam.controls.sharpness":"Sharpness","cam.controls.applySettings":"Apply All Settings","cam.controls.applyAll":"Save Parameters","cam.controls.applyRuntime":"Save Runtime Settings","cam.controls.mode":"Mode Controls","cam.controls.autoExposure":"Exposure Mode","cam.controls.whiteBalance":"White Balance","cam.controls.colorMode":"Color Mode","cam.controls.auto":"Auto","cam.controls.manual":"Manual","cam.controls.night":"Night","cam.controls.off":"Off","cam.controls.nr.off":"Off","cam.controls.nr.fast":"Fast","cam.controls.nr.high_quality":"High quality","cam.controls.wb.auto":"Auto","cam.controls.wb.daylight":"Daylight","cam.controls.wb.cloudy":"Cloudy","cam.controls.wb.tungsten":"Tungsten","cam.controls.wb.fluorescent":"Fluorescent","cam.controls.wb.indoor":"Indoor","cam.controls.wb.manual":"Manual","cam.controls.wb.night":"Night","cam.controls.color":"Color","cam.controls.mono":"Mono","cam.controls.applyAe":"Apply Exposure Mode","cam.controls.applyWb":"Apply White Balance","cam.controls.applyColor":"Apply Color Mode","cam.controls.applyMode":"Apply Mode Settings","cam.controls.lockedByAe":"Exposure and gain are locked while auto exposure is enabled.","cam.controls.lockedByWb":"R/B gains are locked unless white balance is in manual mode.","cam.controls.pendingChanges":"You have unapplied parameter changes","cam.controls.tools":"Tools","cam.controls.nightPreset":"Night Preset","cam.controls.nightOn":"Enable Night Mode","cam.controls.nightOff":"Disable Night Mode","cam.controls.backup":"Backup Settings","cam.controls.restore":"Restore Settings","cam.controls.reset":"Reset Camera","cam.quick.title":"Quick Presets & Smart Tuning","cam.quick.daylight":"Daylight","cam.quick.night":"Night","cam.quick.nightPreset":"Apply Night Preset","cam.quick.deep-sky":"Deep Sky","cam.quick.planetary":"Planetary","cam.quick.autoAdjust":"Auto Adjust","cam.quick.nightHint":"Night preset is one-click tuning; night mode toggle is runtime on/off control.","cam.system.title":"System Monitor","cam.system.status":"Camera Ready","cam.system.stream":"Stream","cam.system.sensor":"Sensor","cam.system.quality":"Image Quality","cam.presets.title":"Preset Management","cam.presets.name":"Preset Name","cam.presets.desc":"Preset Description","cam.presets.save":"Save Preset","cam.presets.empty":"No presets","cam.presets.noDesc":"No description","cam.presets.apply":"Apply","cam.presets.delete":"Delete","cam.files.title":"File Management","cam.files.refresh":"Refresh File List","cam.files.empty":"No files","cam.files.download":"Download","cam.files.info":"Details","cam.files.delete":"Delete","cam.files.loadingInfo":"Loading file details...","cam.files.prev":"Prev","cam.files.next":"Next","cam.files.page":"Page {current}/{total}","cam.files.closeDetail":"Close details","cam.files.size":"Size","cam.files.type":"Type","cam.files.modified":"Modified","cam.hist.title":"Histogram","cam.hist.enabled":"Enable histogram","cam.hist.luminance":"Luminance","cam.hist.over":"Over-exposure","cam.hist.expand":"Expand Histogram","cam.hist.collapse":"Collapse Histogram","cam.notice.previewStart":"Preview started","cam.notice.previewStop":"Preview stopped","cam.notice.captureSaved":"Capture saved: {name}","cam.notice.recordStart":"Recording started: {name}","cam.notice.recordStop":"Recording stopped","cam.notice.fpsApplied":"FPS updated to {fps}","cam.notice.resApplied":"Resolution updated to {w}x{h}","cam.notice.samplingApplied":"Sampling mode updated: {mode}","cam.notice.settingsApplied":"Settings applied","cam.notice.aeApplied":"Exposure mode applied","cam.notice.wbApplied":"White balance applied","cam.notice.colorApplied":"Color mode applied","cam.notice.modeApplied":"Mode settings applied","cam.notice.runtimeApplied":"Runtime settings applied","cam.notice.rotationApplied":"Rotation set to {value}°","cam.mirror.hint":"Use with rotation when the camera is mounted inverted; preview, plate solve, and polar guide share one coordinate frame.","cam.mirror.horizontal":"Horizontal mirror","cam.mirror.vertical":"Vertical mirror","cam.notice.mirrorApplied":"Mirror settings updated","cam.notice.nightOn":"Night mode enabled","cam.notice.nightOff":"Night mode disabled","cam.notice.nightPreset":"Night preset applied","cam.notice.reset":"Camera settings reset","cam.notice.backup":"Settings backed up","cam.notice.restore":"Settings restored","cam.notice.presetSaved":"Preset saved: {name}","cam.notice.presetApplied":"Preset applied: {name}","cam.notice.presetDeleted":"Preset deleted: {name}","cam.notice.download":"Download started: {name}","cam.notice.downloadWithSidecar":"Downloaded: {name} (with sidecar {sidecar})","cam.notice.fileDeleted":"File deleted: {name}","cam.notice.quickPresetApplied":"Quick preset applied: {preset}","cam.notice.needPreview":"Start preview first","cam.notice.needManualForAutoAdjust":"Switch to manual exposure before auto-adjust","cam.notice.autoAdjusted":"Auto-adjust completed and applied","cam.confirm.reset":"Reset camera settings on the current device?","cam.confirm.deletePreset":"Delete preset {name}?","cam.confirm.deleteFile":"Delete file {name}?","sys.sensors.mag.calStatusPrefix":"Calibration mode:","sys.sensors.mag.calModeAuto":"Auto (not locked)","sys.sensors.mag.calModeRecording":"Recording (rotate device)","sys.sensors.mag.calModeLocked":"Locked (saved heading)","sys.sensors.mag.calSamplesPrefix":"Recorded samples:","sys.sensors.mag.calRecordingHint":"Tip: keep level and rotate slowly; save after 10+ samples.","sys.sensors.mag.calLockedHint":"Locked heading params:"},C={"app.title":"OGScope 星空解算控制台","nav.lab":"解算台","nav.labImage":"图片解算","nav.labVideo":"视频解算","nav.pool":"素材池","nav.history":"实验记录","nav.cameraDebug":"相机调试控制台","nav.home":"首页","nav.systemAdmin":"返回系统后台","lang.zh":"中文","lang.en":"EN","sidebar.assets":"自行上传素材","sidebar.upload":"上传文件","sidebar.refresh":"刷新列表","sidebar.debugCaptures":"调试控制台素材","sidebar.assetTypeImage":"图片","sidebar.assetTypeVideo":"视频","sidebar.debugEmpty":"暂无调试文件","sidebar.importToPool":"导入到素材池","sidebar.importToPoolWithTranscode":"转码并导入素材池","sidebar.importToPoolDirect":"直接导入素材池","sidebar.flowPreparing":"准备中…","sidebar.flowImportingDebug":"正在导入调试素材…","sidebar.flowDownloadingPool":"正在下载已导入文件…","sidebar.flowWritingBuffer":"正在写入转码缓冲区…","sidebar.flowLoadingTranscoder":"正在加载转码器…","sidebar.flowTranscoding":"正在转码 AVI -> MP4…","sidebar.flowPackaging":"正在封装转码结果…","sidebar.flowUploadingMp4":"正在上传 MP4…","sidebar.flowReplacing":"正在替换并清理原 AVI…","sidebar.flowNoTranscode":"该文件无需转码,已直接导入。","sidebar.flowDone":"流程完成","sidebar.flowDoneMsg":"已完成:{name}","sidebar.flowFailed":"流程失败","sidebar.debugPage":"第 {cur} / {total} 页","sidebar.batchPresets":"批量预设","sidebar.batchHint":"勾选后点击「批量解算」可一次用多组参数对比结果。","lab.selectOrUpload":"从左侧选择自行上传或已导入的素材","lab.selectOrUploadVideo":"从左侧选择视频素材预览;或使用上方按钮解算设备相机实时帧。","lab.file":"文件","lab.source":"来源","lab.layers":"叠加层","lab.layer.matched":"匹配星","lab.layer.pattern":"图案星","lab.layer.all":"全部质心","lab.layer.rejected":"已剔除(红叉)","lab.previewConfidence":"置信度","lab.grid":"网格","lab.zoomIn":"放大","lab.zoomOut":"缩小","lab.zoomReset":"复位","lab.resolution":"分辨率","lab.fwhm":"FWHM","lab.starsDetected":"检测星点","lab.meta.title":"拍摄与文件信息","lab.meta.noSidecar":"无侧车信息(非调试采集或仅本地上传)","lab.meta.partial":"暂无侧车详细字段,仅显示文件信息。","lab.solveSection":"解算","lab.imageSection":"图像","lab.metric.solveMs":"用时","lab.metric.solveComputeMs":"解算计算用时","lab.metric.solveComputeHelp":"服务端 Tetra3 与提星等纯计算耗时(与网络无关)。","lab.metric.solveRoundTripMs":"全链路用时","lab.metric.solveRoundTripHelp":"从本页发起请求到收到结果并完成界面刷新的总耗时,含网络往返与浏览器渲染。","lab.metric.backendTotalMs":"后端总用时","lab.metric.openDecodeMs":"读取/解码","lab.metric.preprocessMs":"预处理","lab.metric.extractMs":"提星","lab.metric.solveOnlyMs":"匹配解算","lab.metric.probHelp":"Tetra3 的 Prob 为假阳性概率(越低越可信)。一般使用 (1−Prob)×100%;当 Prob 极小时浮点数会舍入为 100%,此时用 −log₁₀(Prob) 映射到约 70–100% 以便区分。","lab.metric.probRawHelp":"Tetra3 返回的原始 Prob 字段,可能为对数似然等内部量;与上一行换算后的百分比对照查看即可。","lab.metric.radec":"RA / Dec","lab.metric.matches":"匹配","lab.metric.rmse":"RMSE","lab.metric.prob":"置信","lab.metric.status":"状态","lab.gateNextMs":"约 {ms} ms 后可重试","lab.gate.skipRecording":"录制中:相机正写入录像,暂无法从相机取帧解算;请先停止录制后再试。","lab.gate.skipBusyInFlight":"上一帧解算仍在处理中,请稍候。若频繁出现,可适当增大「实时解算间隔」。","lab.gate.skipInterval":"解算请求过于频繁,已按后端节流排队。","lab.gate.skipBusyFallback":"实时解算暂时无法开始({detail})。","lab.centroidQualityTitle":"质心质量","lab.centroidQualityMetrics":"输入 {in} → 保留 {out};剔除过密 {dense},共线 {line}","meta.exposure":"曝光","meta.gain":"增益","meta.fps":"帧率","meta.sensor":"传感器","meta.colorMode":"色彩","meta.outputResolution":"输出分辨率","meta.fileTime":"文件时间","meta.fileSize":"文件大小","results.viewRaw":"原始 JSON","results.hideRaw":"收起","results.solveHistoryTitle":"解算历史","results.historySingle":"单帧","results.historyBatch":"批量 {count} 组","params.title":"解算参数","params.blockSolveIntro":"以下为板块求解(Tetra3)搜索天区、超时与粗略指向提示;FOV 需与镜头视场大致一致。","params.centroid":"提星","params.blockCentroidIntro":"以下为星点检测:阈值、连通域面积与局部背景窗口,用于从图像中提取星点质心。","params.fov":"FOV 估计 (°)","params.fovHelp":"水平视场角估计值,用于 lost-in-space 解算。","params.fovErr":"FOV 允许误差 (°)","params.fovErrHelp":"允许 Tetra3 在估计 FOV 附近的搜索范围。","params.timeout":"超时 (ms)","params.timeoutHelp":"单次解算最长等待时间。","params.solveIntervalMs":"实时解算间隔 (ms)","params.solveIntervalMsHelp":"期望间隔可调整,但会被后端限制在安全范围内。","params.solveIntervalIndependent":"解算间隔独立于相机采集和预览帧率。","params.solveIntervalBound":"后端限制范围:{min}-{max} ms,当前生效:{effective} ms","params.centroidRejectionLevel":"质心剔除强度 (1–5)","params.centroidRejectionLevelHelp":"1 最保守保留更多星点,5 更激进剔除过密与共线假星;默认 3。","params.centroidRejectionScale":"保守 ← → 激进","params.solveProfile":"解算档位","params.solveProfileHelp":"速度/平衡/稳健三档会同时调整超时、提星阈值与参与匹配星点数。","params.solveProfileSpeed":"速度优先","params.solveProfileBalanced":"平衡","params.solveProfileRobust":"稳健优先","params.ra":"RA 提示 (°)","params.raHelp":"大致天球赤经,缩小搜索范围(度)。","params.dec":"Dec 提示 (°)","params.decHelp":"大致天球赤纬(度)。","params.maxSide":"提星前长边上界 (px)","params.maxSideHelp":"降采样长边上限,大图可加速提星。","params.detailLevelFull":"包含完整 Tetra3 原始结果(体积略大,仅调试时开启)","params.largeScaleBg":"大尺度背景减除","params.largeScaleBgHelp":"在提星前用低分辨率平滑估计并校正大尺度亮度不均,可减轻角部光晕导致的假星;默认关闭以保持与过往行为一致。","params.sigma":"σ(阈值倍数)","params.sigmaHelp":"高于背景噪声倍数的区域视为星点候选。","params.maxArea":"max_area","params.maxAreaHelp":"连通域最大像素面积。","params.minArea":"min_area","params.minAreaHelp":"连通域最小像素面积。","params.filtsize":"filtsize(奇数)","params.filtsizeHelp":"局部背景滤波窗口边长,须为奇数。","btn.solveOne":"单张解算","btn.solveBatch":"批量解算(勾选预设)","btn.applyPresets":"应用预设到表单","btn.savePreset":"保存","placeholder.newPreset":"新预设名称","pool.title":"服务器素材池","pool.col.name":"文件名","pool.col.source":"来源","pool.col.size":"大小","pool.col.time":"修改时间","pool.delete":"删除","history.title":"实验记录","history.intro":"此处展示你在解算台完成解算后手动保存的快照。用法:在「解算台」主栏「结果对比」中,单张解算后点「保存当前到实验记录」,或批量解算后在某张结果卡片上点「保存记录」。本页可按文件名或预设名搜索,支持导出 JSON/CSV 备份。","history.search":"搜索…","history.searchBtn":"搜索","history.exportJson":"导出 JSON","history.exportCsv":"导出 CSV","history.total":"共 {n} 条","history.preset":"预设","history.metrics":"指标","history.detail":"详情","history.collapse":"收起","history.prev":"上一页","history.next":"下一页","history.delete":"删除","delete.uploadFirst":"确定要从素材池删除「{name}」吗?","delete.uploadSecond":"此操作不可恢复,再次确认删除?","delete.experimentFirst":"确定要删除这条实验记录吗?","delete.experimentSecond":"此操作不可恢复,再次确认删除?","results.title":"结果对比","results.saveCurrent":"保存当前到实验记录","results.saveRow":"保存记录","results.expand":"展开","results.collapseJson":"收起","err.selectFile":"请选择素材","err.selectPresets":"请勾选至少一个预设","common.placeholder":"—","delete.uploadCascade":"该素材有 {n} 条实验记录,是否一并删除?","lab.solveCurrentFrame":"解算当前帧(文件)","lab.solveFileStart":"开始文件连续解算","lab.solveFileStop":"停止文件连续解算","lab.cameraPreviewLoading":"正在连接共享预览…","lab.solveCameraFrame":"解算相机当前帧","lab.solveCameraStart":"开始相机解算","lab.solveCameraStop":"停止相机解算","lab.videoPreviewFailed":"该视频格式可能不受浏览器支持,建议使用 MP4(H.264) 或 WebM。","lab.previewModeFile":"素材文件","lab.previewModeCamera":"设备相机","lab.stopCameraPreview":"停止预览","lab.videoLiveIntro":"与调试控制台共用同一相机;此处可预览并解算实时帧,无需单独打开调试页。两页可同时使用。","lab.videoMjpegHint":"MJPEG 同时连接数已满:请关闭其它页面的实时预览或稍后重试(默认允许约 4 路,可在 systemd 调整 OGSCOPE_STREAM_MAX_MJPEG_CLIENTS)。","lab.cameraStreamStatusFail":"无法读取视频流名额状态,请稍后重试。","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"原始 Prob","lab.systemLoad":"系统负载","results.saveBatchAll":"保存全部到实验记录","lab.transcode.title":"AVI 文件需先转码","lab.transcode.desc":"当前视频为 AVI。为保证浏览器可预览与连续解算,请先在本地转码为 MP4 并上传替换。上传成功后将自动删除服务器上的原 AVI。","lab.transcode.button":"转码并上传替换","lab.transcode.loading":"正在加载转码器…","lab.transcode.writingBuffer":"正在写入转码缓冲区…","lab.transcode.running":"正在转码 AVI -> MP4…","lab.transcode.packaging":"正在封装转码结果…","lab.transcode.uploading":"正在上传转码结果…","lab.transcode.done":"转码并替换完成,可继续预览与解算。","lab.transcode.failed":"转码或上传失败,请重试。","lab.transcode.fetchFailed":"无法读取服务器上的 AVI 文件。","sys.shell.subtitle":"系统调试控制台","sys.shell.nav.overview":"总览","sys.shell.nav.network":"网络","sys.shell.nav.camera":"相机调试","sys.shell.nav.analysis":"寻星控制台","sys.shell.nav.sensors":"传感器","sys.shell.nav.power":"电源","sys.shell.nav.hmi":"人机交互","sys.shell.nav.config":"配置管理","sys.shell.workbench":"WORKBENCH / 系统工作台","sys.shell.node":"节点","sys.shell.top.overview":"系统状态","sys.shell.top.network":"网络与 WiFi","sys.shell.top.sensors":"传感器","sys.shell.top.power":"电源管理","sys.shell.top.hmi":"人机交互","sys.shell.top.config":"配置管理","sys.overview.breadcrumb.console":"控制台","sys.overview.breadcrumb.module":"系统总览","sys.overview.title":"系统总览","sys.overview.subtitle":"实时系统运行健康状况","sys.overview.metric.cpu":"CPU 使用率","sys.overview.metric.mem":"内存占用","sys.overview.metric.temp":"核心温度","sys.overview.metric.wifi":"WiFi 链路","sys.overview.metric.uptime":"运行时长","sys.overview.metric.load":"1 分钟负载","sys.overview.metric.storage":"存储","sys.overview.linkActive":"链路在线","sys.overview.tempState":"状态: 正常","sys.overview.wifiSummary":"无线网络摘要","sys.overview.iface":"接口","sys.overview.signal":"信号","sys.overview.quality":"质量","sys.overview.storageComingSoon":"即将接入","sys.logs.title":"系统日志","sys.logs.kernel":"内核","sys.logs.liveToggle":"实时拉取","sys.logs.refresh":"刷新","sys.logs.liveOffHint":"实时拉取关闭(默认)","sys.logs.loading":"日志加载中...","sys.logs.empty":"暂无日志","sys.placeholder.breadcrumb":"控制台 / 预留模块","sys.placeholder.block":"预留模块","sys.placeholder.desc":"模块壳层已预留,后续将按统一组件体系接入真实数据。","sys.placeholder.status":"STATUS: RESERVED / 当前为结构化占位页面。","sys.placeholder.sensors.title":"传感器诊断台","sys.placeholder.sensors.desc":"用于 IMU、温湿度、电流电压、姿态与时序采样的统一监控面板。","sys.placeholder.sensors.block1":"实时曲线区","sys.placeholder.sensors.block2":"设备健康卡","sys.placeholder.sensors.block3":"阈值告警区","sys.sensors.title":"传感器诊断台","sys.sensors.desc":"磁力计罗盘指北与 MPU-6050 陀螺仪角速度可视化;底层仍为开发者调试 API。","sys.sensors.mag.section":"磁力计(AK09911 / I²C)","sys.sensors.mag.note":"默认总线 1、地址 12(0x0C,CAD 接 GND)。无需在应用层“定义 GPIO”;请确保固件已启用 I²C 且已加载 i2c-dev。","sys.sensors.mag.bus":"I²C 总线号","sys.sensors.mag.addr":"7-bit 地址(十进制,12=0x0C)","sys.sensors.mag.i2cdetect":"附带运行 i2cdetect","sys.sensors.mag.btnSelftest":"运行自检","sys.sensors.mag.btnProbe":"扫描各总线","sys.sensors.mag.btnCalStart":"开始方向校准","sys.sensors.mag.btnCalCommit":"保存并锁定方向","sys.sensors.mag.btnCalReset":"重置到自动模式","sys.sensors.mag.btnCalStatus":"查看校准状态","sys.sensors.mag.running":"执行中…","sys.sensors.mpu.section":"IMU(MPU-6050 / I²C)","sys.sensors.mpu.note":"默认总线 1、地址 104(0x68,AD0 接 GND)。含陀螺仪角速度采样(MPU 内集成加速度计与陀螺仪)。","sys.sensors.mpu.addr":"7-bit 地址(十进制,104=0x68)","sys.sensors.mpu.btnSelftest":"运行 MPU 自检","sys.sensors.mpu.running":"执行中…","sys.sensors.gyro.title":"陀螺仪(MPU-6050)","sys.sensors.gyro.subtitle":"使用上方同一总线/地址;一次读取加速度与陀螺仪,估算横滚/俯仰并显示 3D 板卡示意。","sys.sensors.gyro.btn":"读取角速度","sys.sensors.gyro.loading":"采样中…","sys.sensors.gyro.dps":"角速度","sys.sensors.gyro.raw":"原始值","sys.sensors.gyro.unitDps":"°/s,±250°/s 量程、默认灵敏度","sys.sensors.gyro.unitRaw":"16-bit 有符号","sys.sensors.gyro.hint":"点击「读取角速度」从 IMU 采样(加速度 0x3B + 陀螺仪 0x43)。横滚/俯仰来自重力;ωz 为绕竖直轴角速度,并非绝对航向。","sys.sensors.gyro.errUnknown":"采样失败","sys.sensors.gyro.live":"连续刷新(约 2 Hz)","sys.sensors.gyro.barsTitle":"角速度条(相对 ±250°/s 满偏)","sys.sensors.gyro.rawBlock":"寄存器原始值(16-bit)","sys.sensors.gyro.att3dTitle":"3D 姿态示意","sys.sensors.gyro.att3dDesc":"横滚/俯仰由加速度计重力估算;ωz 为陀螺仪绕竖直轴角速度(非磁航向)。机体系三轴与板卡固连,随姿态在三维空间中转动。","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"左侧为与电路板固连的机体系:琥珀 +X、天蓝 +Y、紫 +Z(法向);短划为负向参考,整体随横滚/俯仰旋转。","sys.sensors.gyro.att3dRefTitle":"正交轴参考(固定视角)","sys.sensors.gyro.att3dRefDesc":"与左侧同色对应,便于辨认三轴空间关系。","sys.sensors.gyro.roll":"横滚","sys.sensors.gyro.pitch":"俯仰","sys.sensors.gyro.yawRate":"偏航角速度","sys.sensors.compass.tapeCaption":"水平航向带 · 中心为当前读数","sys.sensors.compass.title":"磁力计 · 指北罗盘","sys.sensors.compass.desc":"根据水平面磁场分量估算方向角,用于调试「是否大致指向北方」;精对准需校准与固定安装姿态。","sys.sensors.compass.btn":"刷新罗盘","sys.sensors.compass.loading":"测量中…","sys.sensors.compass.live":"连续刷新","sys.sensors.compass.headingLabel":"水平航向角","sys.sensors.compass.headingHint":"由 atan2(Hx, Hy) 得到,单位度","sys.sensors.compass.cardN":"北","sys.sensors.compass.cardE":"东","sys.sensors.compass.cardS":"南","sys.sensors.compass.cardW":"西","sys.sensors.compass.err":"采样失败","sys.sensors.compass.footnote":"红色针尖表示 XY 平面内磁场水平分量方向;转动设备时指针应随之变化。精对准需软铁/硬铁校准。","sys.sensors.jsonToggle":"展开原始 JSON(排障用)","sys.hmi.title":"人机交互 · SPI 显示","sys.hmi.desc":"通过硬件平面驱动 ST7796(默认 320×320);需 OGSCOPE_DISPLAY_ENABLED=true 且系统已启用 SPI(/dev/spidev0.0)。","sys.hmi.status.section":"显示与硬件平面状态","sys.hmi.status.refresh":"刷新状态","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"存在","sys.hmi.status.no":"不存在","sys.hmi.status.resolution":"分辨率 / DC","sys.hmi.status.driver":"驱动句柄","sys.hmi.status.open":"已打开","sys.hmi.status.closed":"未打开","sys.hmi.status.screenOutput":"逻辑输出","sys.hmi.status.on":"开","sys.hmi.status.off":"关","sys.hmi.status.lastPattern":"上次图案","sys.hmi.status.lastError":"上次错误","sys.hmi.actions.section":"输出到屏幕","sys.hmi.actions.hint":"以下为硬件平面 device.command → hmi;大屏刷新使用较长 RPC 超时。","sys.hmi.actions.smoke":"测试画面(文字+边框)","sys.hmi.actions.colorbars":"彩条","sys.hmi.actions.fill":"填充 RGB","sys.hmi.actions.screenOn":"允许屏幕输出","sys.hmi.actions.screenOff":"暂停屏幕输出","sys.hmi.actions.release":"释放驱动(关闭 SPI/GPIO)","sys.hmi.rawJson":"原始 JSON 响应","sys.placeholder.hmi.title":"人机交互台","sys.placeholder.hmi.desc":"用于 SPI 显示、按键矩阵、背光/对比度与界面回放的调试工作区。","sys.placeholder.hmi.block1":"显示预览区","sys.placeholder.hmi.block2":"输入事件流","sys.placeholder.hmi.block3":"参数与模式控制","sys.placeholder.power.title":"电源与功耗台","sys.placeholder.power.desc":"用于电池、充电、瞬时功耗与热管理策略联动调试。","sys.placeholder.power.block1":"功耗看板","sys.placeholder.power.block2":"电源策略表","sys.placeholder.power.block3":"恢复与保护动作","cam.title":"相机调试控制台","cam.subtitle":"迁移到统一后台架构,保留关键性能优化","cam.btn.system":"系统后台","cam.btn.analysis":"寻星控制台","cam.btn.home":"首页","cam.btn.start":"启动预览","cam.btn.starting":"启动中...","cam.btn.stop":"停止预览","cam.btn.stopping":"停止中...","cam.btn.capture":"拍摄","cam.btn.capturing":"拍摄中...","cam.btn.recordStart":"开始录制","cam.btn.recordStop":"停止录制","cam.btn.recordBusy":"录制切换中...","cam.btn.refresh":"刷新状态","cam.preview.title":"实时预览","cam.preview.state":"状态","cam.preview.mode":"曝光模式","cam.preview.emptyTitle":"预览尚未启动","cam.preview.emptyDesc":"点击下方按钮启动相机预览流。","cam.state.streaming":"流已启动","cam.state.idle":"未启动","cam.state.rec":"录制中","cam.stats.requestFps":"请求 FPS","cam.hint.mjpegSingleStream":"MJPEG 同时连接数有限(默认约两路);超出上限时多标签页会争用或返回忙,请减少同时打开的实时预览。","cam.stats.frameFps":"画面 FPS","cam.stats.fpsMeasureNote":"统计最近约 1 秒内在 32×32 采样上的像素变化次数;多路同时预览、网络较慢、或下方「共享流上限」低于相机目标时,读数常明显偏低。","cam.err.streamBusy":"无法连接预览:MJPEG 名额已被占用","cam.err.streamBusyHint":"请关闭其它标签页或解算台中的设备实时预览,或稍后再试。","cam.err.streamProbeFailed":"无法建立预览流","cam.err.streamProbeDetail":"请检查网络与相机服务状态,或稍后重试。","cam.stats.targetFps":"相机采集目标 FPS","cam.stats.streamPacingFps":"共享流/MJPEG 上限 FPS","cam.stats.streamPacingHint":"与进程环境 OGSCOPE_SHARED_PREVIEW_FPS 一致,约束共享抓帧节奏与每路 MJPEG 的最小帧间隔。","cam.stats.captureFps":"实际采集 FPS","cam.stats.encodeMs":"JPEG 平均编码","cam.stats.encoder":"预览编码器","cam.stats.driver":"相机驱动","cam.stats.metadata":"元数据","cam.stats.consumers":"预览/分析/录像消费者","cam.stats.cameraMemory":"进程内存/Swap","cam.stats.longExposureThrottle":"自动曝光约 {exposure} ms,实际帧率受长曝光限制;为保证分析效果未强制缩短曝光。","cam.stats.uptime":"流运行时长","cam.controls.title":"参数控制","cam.controls.core":"核心参数","cam.controls.fps":"帧率","cam.controls.sensorFps":"传感器目标 FPS","cam.controls.previewFps":"预览目标 FPS","cam.controls.resolution":"分辨率","cam.controls.applyRes":"应用分辨率","cam.controls.sampling":"采样模式","cam.controls.exposure":"曝光","cam.controls.gain":"模拟增益","cam.controls.digitalGain":"数字增益","cam.controls.noiseReduction":"降噪级别","cam.controls.noiseReductionMode":"降噪模式","cam.controls.digitalGainReadOnly":"当前驱动仅上报数字增益,不支持手动设置","cam.controls.aeFlicker":"AE 防闪烁","cam.controls.maxAeFrame":"最长自动曝光帧周期(us)","cam.controls.contrast":"对比度","cam.controls.brightness":"亮度","cam.controls.saturation":"饱和度","cam.controls.sharpness":"锐度","cam.controls.applySettings":"应用全部设置","cam.controls.applyAll":"保存参数","cam.controls.applyRuntime":"保存运行参数","cam.controls.mode":"模式控制","cam.controls.autoExposure":"曝光模式","cam.controls.whiteBalance":"白平衡","cam.controls.colorMode":"颜色模式","cam.controls.auto":"自动","cam.controls.manual":"手动","cam.controls.night":"夜间","cam.controls.off":"关闭","cam.controls.nr.off":"关闭","cam.controls.nr.fast":"快速","cam.controls.nr.high_quality":"高质量","cam.controls.wb.auto":"自动","cam.controls.wb.daylight":"日光","cam.controls.wb.cloudy":"阴天","cam.controls.wb.tungsten":"钨丝灯","cam.controls.wb.fluorescent":"荧光灯","cam.controls.wb.indoor":"室内","cam.controls.wb.manual":"手动","cam.controls.wb.night":"夜间","cam.controls.color":"彩色","cam.controls.mono":"黑白","cam.controls.applyAe":"应用曝光模式","cam.controls.applyWb":"应用白平衡","cam.controls.applyColor":"应用颜色模式","cam.controls.applyMode":"应用模式设置","cam.controls.lockedByAe":"自动曝光开启时,曝光与增益参数锁定。","cam.controls.lockedByWb":"当前非手动白平衡模式,R/B 增益参数锁定。","cam.controls.pendingChanges":"有未应用的参数修改","cam.controls.tools":"工具","cam.controls.nightPreset":"夜间预设","cam.controls.nightOn":"开启夜间模式","cam.controls.nightOff":"关闭夜间模式","cam.controls.backup":"备份设置","cam.controls.restore":"恢复设置","cam.controls.reset":"重置相机","cam.quick.title":"快速预设与智能调参","cam.quick.daylight":"白天模式","cam.quick.night":"夜间模式","cam.quick.nightPreset":"应用夜间预设","cam.quick.deep-sky":"深空模式","cam.quick.planetary":"行星模式","cam.quick.autoAdjust":"智能调整","cam.quick.nightHint":"夜间预设用于一键切换夜拍参数;夜间模式开关用于实时启停红外/夜景模式。","cam.system.title":"系统监控","cam.system.status":"相机就绪","cam.system.stream":"流状态","cam.system.sensor":"传感器","cam.system.quality":"图像质量","cam.presets.title":"预设管理","cam.presets.name":"预设名称","cam.presets.desc":"预设描述","cam.presets.save":"保存预设","cam.presets.empty":"暂无预设","cam.presets.noDesc":"无描述","cam.presets.apply":"应用","cam.presets.delete":"删除","cam.files.title":"文件管理","cam.files.refresh":"刷新文件列表","cam.files.empty":"暂无文件","cam.files.download":"下载","cam.files.info":"详情","cam.files.delete":"删除","cam.files.loadingInfo":"正在读取文件详情...","cam.files.prev":"上一页","cam.files.next":"下一页","cam.files.page":"第 {current}/{total} 页","cam.files.closeDetail":"关闭详情","cam.files.size":"文件大小","cam.files.type":"文件类型","cam.files.modified":"修改时间","cam.hist.title":"直方图","cam.hist.enabled":"启用直方图","cam.hist.luminance":"亮度通道","cam.hist.over":"过曝警告","cam.hist.expand":"展开直方图","cam.hist.collapse":"收起直方图","cam.notice.previewStart":"预览已启动","cam.notice.previewStop":"预览已停止","cam.notice.captureSaved":"拍摄完成: {name}","cam.notice.recordStart":"开始录制: {name}","cam.notice.recordStop":"录制已停止","cam.notice.fpsApplied":"帧率已设置为 {fps}","cam.notice.resApplied":"分辨率已设置为 {w}x{h}","cam.notice.samplingApplied":"采样模式已设置为 {mode}","cam.notice.settingsApplied":"参数已应用","cam.notice.aeApplied":"曝光模式已应用","cam.notice.wbApplied":"白平衡已应用","cam.notice.colorApplied":"颜色模式已应用","cam.notice.modeApplied":"模式设置已应用","cam.notice.runtimeApplied":"运行参数已应用","cam.notice.rotationApplied":"旋转已设置为 {value}°","cam.mirror.hint":"相机倒装时可配合旋转使用;镜像后预览与星点解算、极轴引导同坐标系。","cam.mirror.horizontal":"水平镜像","cam.mirror.vertical":"垂直镜像","cam.notice.mirrorApplied":"镜像设置已更新","cam.notice.nightOn":"夜间模式已开启","cam.notice.nightOff":"夜间模式已关闭","cam.notice.nightPreset":"夜间预设已应用","cam.notice.reset":"相机设置已重置","cam.notice.backup":"设置已备份","cam.notice.restore":"设置已恢复","cam.notice.presetSaved":"预设已保存: {name}","cam.notice.presetApplied":"预设已应用: {name}","cam.notice.presetDeleted":"预设已删除: {name}","cam.notice.download":"已开始下载: {name}","cam.notice.downloadWithSidecar":"已下载: {name}(含侧车 {sidecar})","cam.notice.fileDeleted":"文件已删除: {name}","cam.notice.quickPresetApplied":"已应用快速预设: {preset}","cam.notice.needPreview":"请先启动预览","cam.notice.needManualForAutoAdjust":"请先切换为手动曝光再进行智能调整","cam.notice.autoAdjusted":"智能调整已完成并生效","cam.confirm.reset":"将重置当前设备的相机设置。确认继续?","cam.confirm.deletePreset":"确定删除预设 {name} 吗?","cam.confirm.deleteFile":"确定删除文件 {name} 吗?","sys.sensors.mag.calStatusPrefix":"校准状态:","sys.sensors.mag.calModeAuto":"自动模式(未锁定)","sys.sensors.mag.calModeRecording":"录制中(请旋转设备)","sys.sensors.mag.calModeLocked":"已锁定(使用保存方向)","sys.sensors.mag.calSamplesPrefix":"录制样本:","sys.sensors.mag.calRecordingHint":"操作提示:保持水平,缓慢旋转设备,建议累计 10+ 样本后保存。","sys.sensors.mag.calLockedHint":"已锁定方向参数:"},g=a.createContext(null),p={zh:C,en:k},b="ogscope.analysis.locale";function y(){const e=window.localStorage.getItem(b);return e==="zh"||e==="en"?e:(navigator.language||"zh").toLowerCase().startsWith("en")?"en":"zh"}function R({children:e}){const[s,t]=a.useState(y),[o,l]=a.useState(p[y()]);a.useEffect(()=>{l(p[s]),window.localStorage.setItem(b,s),document.documentElement.lang=s==="en"?"en":"zh-CN"},[s]);const r=a.useMemo(()=>(n,c)=>{let i=o[n]??n;if(c)for(const[d,h]of Object.entries(c))i=i.replace(new RegExp(`\\{${d}\\}`,"g"),String(h));return i},[o]),m=a.useMemo(()=>({locale:s,setLocale:t,t:r}),[s,r]);return v.jsx(g.Provider,{value:m,children:e})}function x(){const e=a.useContext(g);if(!e)throw new Error("useI18n must be used within I18nProvider");return e}export{R as I,A as T,P as c,x as u}; diff --git a/web/static/analysis-lab/assets/index-DstXdMlD.css b/web/static/analysis-lab/assets/index-DstXdMlD.css deleted file mode 100644 index 347ebac..0000000 --- a/web/static/analysis-lab/assets/index-DstXdMlD.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-3{left:.75rem}.left-\[10px\]{left:10px}.left-\[110px\]{left:110px}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-12{top:3rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-8{top:2rem}.top-\[18px\]{top:18px}.top-\[94px\]{top:94px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.order-1{order:1}.order-3{order:3}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-0{height:0px}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100px\]{height:100px}.h-\[112px\]{height:112px}.h-\[152px\]{height:152px}.h-\[188px\]{height:188px}.h-\[460px\]{height:460px}.h-\[88px\]{height:88px}.h-\[calc\(100vh-7rem\)\]{height:calc(100vh - 7rem)}.h-dvh{height:100dvh}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[320px\]{max-height:320px}.max-h-\[420px\]{max-height:420px}.max-h-\[70vh\]{max-height:70vh}.max-h-\[min\(22rem\,55vh\)\]{max-height:min(22rem,55vh)}.max-h-\[min\(36vh\,18rem\)\]{max-height:min(36vh,18rem)}.max-h-\[min\(50vh\,28rem\)\]{max-height:min(50vh,28rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0px}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[480px\]{min-height:480px}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100px\]{width:100px}.w-\[200px\]{width:200px}.w-\[220px\]{width:220px}.w-\[360px\]{width:360px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[15rem\]{min-width:15rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[1880px\]{max-width:1880px}.max-w-\[220px\]{max-width:220px}.max-w-\[calc\(100\%-1\.5rem\)\]{max-width:calc(100% - 1.5rem)}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.border-separate{border-collapse:separate}.border-spacing-y-2{--tw-border-spacing-y: .5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-top-left{transform-origin:top left}.-translate-x-px{--tw-translate-x: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-x-\[9px\]{border-left-width:9px;border-right-width:9px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-b-\[12px\]{border-bottom-width:12px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-2{border-right-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-amber-500\/40{border-color:#f59e0b66}.border-emerald-400\/40{border-color:#34d39966}.border-emerald-500\/20{border-color:#10b98133}.border-emerald-600\/35{border-color:#05966959}.border-error\/35{border-color:#ffb4ab59}.border-error\/40{border-color:#ffb4ab66}.border-error\/50{border-color:#ffb4ab80}.border-error\/60{border-color:#ffb4ab99}.border-outline-variant\/10{border-color:#4147541a}.border-outline-variant\/15{border-color:#41475426}.border-outline-variant\/20{border-color:#41475433}.border-outline-variant\/25{border-color:#41475440}.border-outline-variant\/30{border-color:#4147544d}.border-outline-variant\/40{border-color:#41475466}.border-primary{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.border-primary\/30{border-color:#adc6ff4d}.border-primary\/40{border-color:#adc6ff66}.border-primary\/50{border-color:#adc6ff80}.border-primary\/70{border-color:#adc6ffb3}.border-red-500\/40{border-color:#ef444466}.border-rose-400\/40{border-color:#fb718566}.border-sky-500\/30{border-color:#0ea5e94d}.border-teal-400\/75{border-color:#2dd4bfbf}.border-teal-500\/25{border-color:#14b8a640}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-white\/5{border-color:#ffffff0d}.border-x-transparent{border-left-color:transparent;border-right-color:transparent}.border-b-amber-400{--tw-border-opacity: 1;border-bottom-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-400\/95{background-color:#fbbf24f2}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-900\/55{background-color:#78350f8c}.bg-background{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-black\/25{background-color:#00000040}.bg-black\/35{background-color:#00000059}.bg-black\/40{background-color:#0006}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/80{background-color:#000c}.bg-emerald-800\/90{background-color:#065f46e6}.bg-error-container\/15{background-color:#93000a26}.bg-error-container\/20{background-color:#93000a33}.bg-error-container\/95{background-color:#93000af2}.bg-error\/10{background-color:#ffb4ab1a}.bg-error\/80{background-color:#ffb4abcc}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-neutral-950\/80{background-color:#0a0a0acc}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(173 198 255 / var(--tw-bg-opacity, 1))}.bg-primary-container{--tw-bg-opacity: 1;background-color:rgb(76 142 255 / var(--tw-bg-opacity, 1))}.bg-primary-container\/20{background-color:#4c8eff33}.bg-primary-container\/30{background-color:#4c8eff4d}.bg-primary-container\/50{background-color:#4c8eff80}.bg-primary\/10{background-color:#adc6ff1a}.bg-primary\/20{background-color:#adc6ff33}.bg-primary\/5{background-color:#adc6ff0d}.bg-red-500\/10{background-color:#ef44441a}.bg-secondary{--tw-bg-opacity: 1;background-color:rgb(64 229 108 / var(--tw-bg-opacity, 1))}.bg-secondary\/80{background-color:#40e56ccc}.bg-sky-300\/90{background-color:#7dd3fce6}.bg-sky-400{--tw-bg-opacity: 1;background-color:rgb(56 189 248 / var(--tw-bg-opacity, 1))}.bg-sky-400\/95{background-color:#38bdf8f2}.bg-sky-700\/80{background-color:#0369a1cc}.bg-sky-900\/50{background-color:#0c4a6e80}.bg-slate-500\/85{background-color:#64748bd9}.bg-slate-500\/90{background-color:#64748be6}.bg-slate-600\/40{background-color:#47556966}.bg-slate-600\/50{background-color:#47556980}.bg-slate-600\/60{background-color:#47556999}.bg-slate-900\/80{background-color:#0f172acc}.bg-slate-950\/50{background-color:#02061780}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-surface-container{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.bg-surface-container-high{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.bg-surface-container-high\/40{background-color:#272a3166}.bg-surface-container-high\/95{background-color:#272a31f2}.bg-surface-container-highest{--tw-bg-opacity: 1;background-color:rgb(50 53 60 / var(--tw-bg-opacity, 1))}.bg-surface-container-highest\/30{background-color:#32353c4d}.bg-surface-container-highest\/40{background-color:#32353c66}.bg-surface-container-highest\/95{background-color:#32353cf2}.bg-surface-container-low{--tw-bg-opacity: 1;background-color:rgb(25 28 34 / var(--tw-bg-opacity, 1))}.bg-surface-container-low\/80{background-color:#191c22cc}.bg-surface-container-low\/90{background-color:#191c22e6}.bg-surface-container-lowest{--tw-bg-opacity: 1;background-color:rgb(11 14 20 / var(--tw-bg-opacity, 1))}.bg-surface-container-lowest\/90{background-color:#0b0e14e6}.bg-surface-container-lowest\/95{background-color:#0b0e14f2}.bg-surface-container\/50{background-color:#1d202680}.bg-surface-container\/60{background-color:#1d202699}.bg-surface-container\/80{background-color:#1d2026cc}.bg-surface-container\/90{background-color:#1d2026e6}.bg-violet-400{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity, 1))}.bg-violet-400\/95{background-color:#a78bfaf2}.bg-violet-900\/45{background-color:#4c1d9573}.bg-white\/5{background-color:#ffffff0d}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-300\/95{--tw-gradient-from: rgb(252 211 77 / .95) var(--tw-gradient-from-position);--tw-gradient-to: rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-emerald-600{--tw-gradient-from: #059669 var(--tw-gradient-from-position);--tw-gradient-to: rgb(5 150 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-600\/90{--tw-gradient-from: rgb(71 85 105 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/40{--tw-gradient-from: rgb(15 23 42 / .4) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/90{--tw-gradient-from: rgb(15 23 42 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-slate-800\/95{--tw-gradient-to: rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(30 41 59 / .95) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-slate-900\/70{--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(15 23 42 / .7) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-sky-400\/40{--tw-gradient-to: rgb(56 189 248 / .4) var(--tw-gradient-to-position)}.to-sky-500{--tw-gradient-to: #0ea5e9 var(--tw-gradient-to-position)}.to-slate-950{--tw-gradient-to: #020617 var(--tw-gradient-to-position)}.to-surface-container\/70{--tw-gradient-to: rgb(29 32 38 / .7) var(--tw-gradient-to-position)}.to-surface-container\/90{--tw-gradient-to: rgb(29 32 38 / .9) var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0\.5{padding-right:.125rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-body,.font-headline{font-family:system-ui,Segoe UI,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity, 1))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-200\/95{color:#fde68af2}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.text-on-primary-container{--tw-text-opacity: 1;color:rgb(0 40 93 / var(--tw-text-opacity, 1))}.text-on-surface{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.text-on-surface-variant{--tw-text-opacity: 1;color:rgb(194 198 214 / var(--tw-text-opacity, 1))}.text-on-surface-variant\/70{color:#c2c6d6b3}.text-on-surface-variant\/80{color:#c2c6d6cc}.text-on-surface-variant\/85{color:#c2c6d6d9}.text-on-surface-variant\/90{color:#c2c6d6e6}.text-on-surface-variant\/95{color:#c2c6d6f2}.text-on-surface\/80{color:#e1e2ebcc}.text-primary{--tw-text-opacity: 1;color:rgb(173 198 255 / var(--tw-text-opacity, 1))}.text-primary\/80{color:#adc6ffcc}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(64 229 108 / var(--tw-text-opacity, 1))}.text-sky-100{--tw-text-opacity: 1;color:rgb(224 242 254 / var(--tw-text-opacity, 1))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.text-sky-300\/90{color:#7dd3fce6}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-teal-100{--tw-text-opacity: 1;color:rgb(204 251 241 / var(--tw-text-opacity, 1))}.text-teal-200{--tw-text-opacity: 1;color:rgb(153 246 228 / var(--tw-text-opacity, 1))}.text-teal-200\/95{color:#99f6e4f2}.text-violet-200{--tw-text-opacity: 1;color:rgb(221 214 254 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-primary{accent-color:#adc6ff}.opacity-50{opacity:.5}.opacity-90{opacity:.9}.shadow-\[0_0_6px_rgba\(255\,255\,255\,0\.6\)\]{--tw-shadow: 0 0 6px rgba(255,255,255,.6);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_20px_50px_rgba\(0\,0\,0\,0\.55\)\,inset_0_1px_0_rgba\(255\,255\,255\,0\.08\)\]{--tw-shadow: 0 20px 50px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);--tw-shadow-colored: 0 20px 50px var(--tw-shadow-color), inset 0 1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/35{--tw-ring-color: rgb(253 230 138 / .35)}.ring-sky-200\/30{--tw-ring-color: rgb(186 230 253 / .3)}.ring-sky-500\/25{--tw-ring-color: rgb(14 165 233 / .25)}.ring-slate-600\/50{--tw-ring-color: rgb(71 85 105 / .5)}.ring-teal-500\/10{--tw-ring-color: rgb(20 184 166 / .1)}.ring-violet-200\/35{--tw-ring-color: rgb(221 214 254 / .35)}.ring-white\/5{--tw-ring-color: rgb(255 255 255 / .05)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}html,body,#root{height:100%}.og-scrollbar{scrollbar-width:thin;scrollbar-color:#414754 #191c22}.og-scrollbar::-webkit-scrollbar{width:7px;height:7px}.og-scrollbar::-webkit-scrollbar-track{background:#191c22;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb{background:#414754;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb:hover{background:#5c6070}.hover\:border-primary:hover{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.hover\:bg-amber-500\/10:hover{background-color:#f59e0b1a}.hover\:bg-emerald-500\/10:hover{background-color:#10b9811a}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity, 1))}.hover\:bg-error-container\/20:hover{background-color:#93000a33}.hover\:bg-error-container\/30:hover{background-color:#93000a4d}.hover\:bg-error\/10:hover{background-color:#ffb4ab1a}.hover\:bg-rose-500\/10:hover{background-color:#f43f5e1a}.hover\:bg-sky-600:hover{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container:hover{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container-high:hover{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container\/80:hover{background-color:#1d2026cc}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-error:hover{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.hover\:text-on-surface:hover{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-4{gap:1rem}}@media (min-width: 768px){.md\:fixed{position:fixed}.md\:left-0{left:0}.md\:top-0{top:0}.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:ml-64{margin-left:16rem}.md\:block{display:block}.md\:h-full{height:100%}.md\:w-64{width:16rem}.md\:min-w-\[200px\]{min-width:200px}.md\:max-w-md{max-width:28rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-center{justify-content:center}.md\:gap-8{gap:2rem}.md\:border-b-0{border-bottom-width:0px}.md\:border-r{border-right-width:1px}.md\:border-white\/5{border-color:#ffffff0d}.md\:p-6{padding:1.5rem}.md\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:max-w-lg{max-width:32rem}}@media (min-width: 1280px){.xl\:order-1{order:1}.xl\:order-2{order:2}.xl\:col-span-10{grid-column:span 10 / span 10}.xl\:col-span-2{grid-column:span 2 / span 2}.xl\:col-span-3{grid-column:span 3 / span 3}.xl\:col-span-9{grid-column:span 9 / span 9}.xl\:max-w-\[340px\]{max-width:340px}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:gap-8{gap:2rem}.xl\:self-start{align-self:flex-start}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none} diff --git a/web/static/analysis-lab/assets/refresh-cw-gseCLMRP.js b/web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js similarity index 91% rename from web/static/analysis-lab/assets/refresh-cw-gseCLMRP.js rename to web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js index 0ea1c11..57f12de 100644 --- a/web/static/analysis-lab/assets/refresh-cw-gseCLMRP.js +++ b/web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js @@ -1,4 +1,4 @@ -import{c as e}from"./index-C78KOEFu.js";/** +import{c as e}from"./index-CV2LaaT8.js";/** * @license lucide-react v0.460.0 - ISC * * This source code is licensed under the ISC license. diff --git a/web/static/analysis-lab/assets/system-BMLXYbaG.js b/web/static/analysis-lab/assets/system-BMLXYbaG.js new file mode 100644 index 0000000..217e731 --- /dev/null +++ b/web/static/analysis-lab/assets/system-BMLXYbaG.js @@ -0,0 +1,83 @@ +import{r,j as e,a as Re,R as Oe}from"./client-D1ZVDB-N.js";import{u as ye,X as Te,C as Ae,r as K,a as W,S as Fe,b as ze}from"./http-lbh6zBy9.js";import{c as U,u as V,T as Ie,I as De}from"./index-CV2LaaT8.js";import{R as ve}from"./refresh-cw-CIJtimxg.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 ge=U("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 He=U("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 qe=U("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 Ee=U("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 Ue=U("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 We=U("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 Be=U("Menu",[["line",{x1:"4",x2:"20",y1:"12",y2:"12",key:"1e0a9i"}],["line",{x1:"4",x2:"20",y1:"6",y2:"6",key:"1owob3"}],["line",{x1:"4",x2:"20",y1:"18",y2:"18",key:"yk5zj1"}]]);/** + * @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 Ke=U("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 Xe=U("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 Pe=U("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 Ge=U("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 we=U("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 Je=U("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 Ye=U("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 Ze=U("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 je=U("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"}]]),te=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 Ve({route:t,allowNetworkRoute:i,onRouteChange:s,children:a}){const{t:n,locale:d,setLocale:g}=V(),{info:h}=ye(),[f,p]=r.useState(!1),C=(h==null?void 0:h.cpu_usage)!=null?Number(h.cpu_usage).toFixed(1):"—",y=(h==null?void 0:h.memory_usage)!=null?Number(h.memory_usage).toFixed(1):"—",M=(h==null?void 0:h.temperature)!=null?Number(h.temperature).toFixed(1):"—",$=(h==null?void 0:h.wifi_quality)!=null&&!Number.isNaN(Number(h.wifi_quality))?`${Number(h.wifi_quality).toFixed(0)}%`:"—",u={overview:n("sys.shell.top.overview"),network:n("sys.shell.top.network"),sensors:n("sys.shell.top.sensors"),hmi:n("sys.shell.top.hmi"),power:n("sys.shell.top.power"),config:n("sys.shell.top.config")},E=te(!1),O=(k,_)=>{const b=window.open(k,_);b&&b.focus()};return e.jsxs("div",{className:"flex h-full min-h-0 flex-col bg-background text-on-surface md:flex-row",children:[f?e.jsx("button",{type:"button",className:"fixed inset-0 z-40 bg-black/60 lg:hidden","aria-label":d==="zh"?"关闭导航":"Close navigation",onClick:()=>p(!1)}):null,e.jsxs("aside",{id:"system-console-navigation",className:`glass-panel fixed inset-y-0 left-0 z-50 flex w-64 shrink-0 flex-col overflow-y-auto border-r border-white/5 bg-surface-container-low/95 backdrop-blur-xl transition-transform lg:translate-x-0 ${f?"translate-x-0":"-translate-x-full"}`,children:[e.jsxs("div",{className:"p-5",children:[e.jsx("div",{className:"mb-3 flex justify-end lg:hidden",children:e.jsx("button",{type:"button",className:"rounded p-1.5 text-on-surface-variant hover:bg-white/5 hover:text-on-surface","aria-label":d==="zh"?"关闭导航":"Close navigation",onClick:()=>p(!1),children:e.jsx(Te,{className:"h-5 w-5"})})}),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(we,{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:n("sys.shell.subtitle")})]})]}),e.jsxs("nav",{className:"flex flex-col gap-0.5",children:[e.jsxs("button",{type:"button",className:te(t==="overview"),onClick:()=>{s("overview"),p(!1)},children:[e.jsx(We,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.overview")})]}),i&&e.jsxs("button",{type:"button",className:te(t==="network"),onClick:()=>{s("network"),p(!1)},children:[e.jsx(Ke,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.network")})]}),e.jsxs("a",{href:"/debug/camera",className:E,onClick:k=>{k.preventDefault(),O("/debug/camera","ogscopeCameraConsole")},children:[e.jsx(Ae,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.camera")})]}),e.jsxs("a",{href:"/debug/analysis",className:E,onClick:k=>{k.preventDefault(),O("/debug/analysis","ogscopeAnalysisConsole")},children:[e.jsx(we,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.analysis")})]}),e.jsxs("button",{type:"button",className:te(t==="sensors"),onClick:()=>{s("sensors"),p(!1)},children:[e.jsx(ge,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.sensors")})]}),e.jsxs("button",{type:"button",className:te(t==="power"),onClick:()=>{s("power"),p(!1)},children:[e.jsx(He,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.power")})]}),e.jsxs("button",{type:"button",className:te(t==="hmi"),onClick:()=>{s("hmi"),p(!1)},children:[e.jsx(Ye,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.hmi")})]}),e.jsxs("button",{type:"button",className:te(t==="config"),onClick:()=>{s("config"),p(!1)},children:[e.jsx(Ge,{className:"h-4 w-4 shrink-0"}),e.jsx("span",{children:n("sys.shell.nav.config")})]})]})]}),e.jsx("div",{className:"mt-auto p-5",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",title:n("sys.shell.workbench"),children:n("sys.shell.workbench")}),e.jsxs("p",{className:"font-mono text-[10px] text-on-surface-variant",children:[n("sys.shell.node"),": OGSCOPE_PI_ZERO_2W"]})]})})]}),e.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1 flex-col lg:ml-64",children:[e.jsxs("header",{className:"sticky top-0 z-30 flex min-h-14 shrink-0 flex-wrap items-center justify-between gap-2 border-b border-white/5 bg-neutral-950/80 px-4 py-2 backdrop-blur-md lg:px-8",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-3",children:[e.jsx("button",{type:"button",className:"rounded p-1.5 text-on-surface-variant hover:bg-white/5 hover:text-on-surface lg:hidden","aria-controls":"system-console-navigation","aria-expanded":f,"aria-label":d==="zh"?"打开导航":"Open navigation",onClick:()=>p(!0),children:e.jsx(Be,{className:"h-5 w-5"})}),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:u[t]})]}),e.jsxs("div",{className:"flex min-w-0 flex-wrap items-center justify-end gap-2 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:()=>g("zh"),children:n("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:()=>g("en"),children:n("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(Ee,{className:"h-3.5 w-3.5"})," CPU ",C,"%"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx(ge,{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"})," ",M]}),e.jsxs("span",{className:"flex items-center gap-1 text-secondary",children:[e.jsx(je,{className:"h-3.5 w-3.5"})," ",$]})]})]})]}),e.jsx("main",{className:"og-scrollbar min-h-0 flex-1 overflow-auto p-4 lg:p-6",children:a})]})]})}async function Qe(){return await K("/api/dev/system/hardware-plane/status",{cache:"no-store"})}async function es(t){return await K("/api/dev/system/hardware-plane/command",{method:"POST",body:JSON.stringify(t)})}async function ss(t){var a;const i=new URLSearchParams;t!=null&&t.service&&i.set("service",t.service),i.set("since_seconds",String(t.sinceSeconds)),i.set("limit",String(t.limit)),(a=t==null?void 0:t.levels)!=null&&a.length&&i.set("levels",t.levels.join(","));const s=i.toString();return await K(`/api/dev/debug/logs/systemd${s?`?${s}`:""}`,{cache:"no-store"})}function ts(t){const i=Math.max(0,parseInt(String(t??0),10)||0),s=Math.floor(i/86400),a=Math.floor(i%86400/3600),n=Math.floor(i%3600/60);return s>0?`${s}d ${a}h`:a>0?`${a}h ${n}m`:`${n}m`}function he(t,i=1){return t==null||Number.isNaN(Number(t))?"—":Number(t).toFixed(i)}function ke(t){return t==="ERROR"?"text-error":t==="WARN"?"text-amber-300":"text-primary"}function as(t){if(!t)return"--:--:--";const i=new Date(t);return Number.isNaN(i.getTime())?"--:--:--":i.toLocaleTimeString()}function rs(){const{t:s}=V(),{info:a,error:n}=ye(),[d,g]=r.useState(!1),[h,f]=r.useState(["INFO","WARN","ERROR"]),[p,C]=r.useState([]),[y,M]=r.useState(null),[$,u]=r.useState(!1),[E,O]=r.useState(!0),k=r.useRef(null),_=r.useRef(new Set),b=he(a==null?void 0:a.cpu_usage),R=he(a==null?void 0:a.memory_usage),I=he(a==null?void 0:a.temperature),v=(a==null?void 0:a.load_average_1m)!=null?String(a.load_average_1m):"—",P=(a==null?void 0:a.wifi_quality)!=null&&!Number.isNaN(Number(a.wifi_quality))?`${Number(a.wifi_quality).toFixed(0)}%`:"—",N=(a==null?void 0:a.wifi_signal_dbm)!=null&&!Number.isNaN(Number(a.wifi_signal_dbm))?`${Number(a.wifi_signal_dbm).toFixed(0)} dBm`:"—",L=async()=>{if(h.length===0){C([]);return}u(!0);try{M(null);const c=await ss({service:"ogscope",sinceSeconds:1200,limit:240,levels:h});C(S=>{const T=new Set(S.map(q=>`${q.ts??""}::${q.level}::${q.source}::${q.message}`)),F=[...S];for(const q of c.items){const G=`${q.ts??""}::${q.level}::${q.source}::${q.message}`;T.has(G)||(T.add(G),F.push(q))}return F.length<=300?F:F.slice(F.length-300)})}catch(c){M(c instanceof Error?c.message:String(c))}finally{u(!1)}};r.useEffect(()=>{if(!d)return;L();const c=window.setInterval(()=>{document.hidden||L()},4e3);return()=>window.clearInterval(c)},[d,h.join(",")]),r.useEffect(()=>{const c=k.current;if(!c)return;const S=()=>{const T=c.scrollHeight-c.scrollTop-c.clientHeight;O(T<=24)};return S(),c.addEventListener("scroll",S),()=>c.removeEventListener("scroll",S)},[]),r.useEffect(()=>{if(!k.current||p.length===0)return;const S=new Set(p.map(F=>`${F.ts??""}::${F.level}::${F.source}::${F.message}`));let T=!1;S.forEach(F=>{_.current.has(F)||(T=!0)}),_.current=S,E&&T&&requestAnimationFrame(()=>{k.current&&(k.current.scrollTop=k.current.scrollHeight)})},[p,E]);const D=r.useMemo(()=>new Set(h),[h]);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:s("sys.overview.breadcrumb.console")}),e.jsx("span",{children:"/"}),e.jsx("span",{className:"text-primary",children:s("sys.overview.breadcrumb.module")})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:s("sys.overview.title")}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:s("sys.overview.subtitle")})]}),n&&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:n}),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:s("sys.overview.metric.cpu")}),e.jsx(Ee,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[b,"%"]}),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(b)||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:s("sys.overview.metric.mem")}),e.jsx(ge,{className:"h-4 w-4 text-secondary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[R,"%"]}),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(R)||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:s("sys.overview.metric.temp")}),e.jsx(Je,{className:"h-4 w-4 text-primary"})]}),e.jsxs("div",{className:"text-3xl font-bold text-on-surface",children:[I,"°C"]}),e.jsx("div",{className:"mt-3 text-xs text-on-surface-variant",children:s("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:s("sys.overview.metric.wifi")}),e.jsx(je,{className:"h-4 w-4 text-primary"})]}),e.jsx("div",{className:"text-3xl font-bold text-on-surface",children:P}),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:s("sys.overview.wifiSummary")}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[s("sys.overview.iface"),": ",String((a==null?void 0:a.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:s("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:s("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:s("sys.overview.quality")}),e.jsx("p",{className:"mt-1 font-mono text-2xl font-bold",children:P})]})]})]}),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:s("sys.overview.metric.uptime")}),e.jsx("p",{className:"mt-1 text-xl font-bold",children:ts(a==null?void 0:a.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:s("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:s("sys.overview.metric.storage")}),e.jsx(Ue,{className:"h-4 w-4 text-on-surface-variant"})]}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:s("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:s("sys.logs.title")}),e.jsxs("span",{className:"text-[10px] text-on-surface-variant",children:[s("sys.logs.kernel"),": ",String((a==null?void 0:a.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:c=>g(c.target.checked)}),s("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 L(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(ve,{className:"h-3.5 w-3.5"})," ",s("sys.logs.refresh")]})})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-2 text-xs",children:[["INFO","WARN","ERROR"].map(c=>e.jsxs("label",{className:"inline-flex items-center gap-1.5 text-on-surface-variant",children:[e.jsx("input",{type:"checkbox",checked:D.has(c),onChange:S=>{f(T=>S.target.checked?Array.from(new Set([...T,c])):T.filter(F=>F!==c))}}),e.jsx("span",{className:ke(c),children:c})]},c)),!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(Ze,{className:"h-3.5 w-3.5"})," ",s("sys.logs.liveOffHint")]})]}),y&&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:y}),e.jsxs("div",{ref:k,className:"og-scrollbar max-h-72 space-y-1 overflow-auto font-mono text-[11px] text-on-surface-variant",children:[$&&p.length===0&&e.jsx("div",{children:s("sys.logs.loading")}),!$&&p.length===0&&e.jsx("div",{children:s("sys.logs.empty")}),p.map((c,S)=>e.jsxs("div",{className:"flex items-start gap-2",children:[e.jsxs("span",{className:"shrink-0 text-primary",children:["[",as(c.ts),"]"]}),e.jsx("span",{className:`shrink-0 ${ke(c.level)}`,children:c.level}),e.jsx("span",{className:"shrink-0 text-on-surface/80",children:c.source}),e.jsx("span",{className:"min-w-0 break-words text-on-surface",children:c.message})]},`${c.ts||"ts"}-${S}`))]})]})]})}function me(){return typeof window.OGSCOPE_HTTP_PORT=="number"?window.OGSCOPE_HTTP_PORT:8e3}function ns(t,i){const s=Math.max(0,parseInt(String(t??0),10)||0),a=Math.floor(s/86400),n=Math.floor(s%86400/3600),d=Math.floor(s%3600/60);return a>0?i?`${a}天 ${n}小时`:`${a}d ${n}h`:n>0?i?`${n}小时 ${d}分`:`${n}h ${d}m`:i?`${d}分`:`${d}m`}function os(){const{locale:t}=V(),i=t==="zh",s=r.useCallback((l,j)=>i?l:j,[i]),{info:a,error:n}=ye(),[d,g]=r.useState(()=>s("加载中...","Loading...")),[h,f]=r.useState(""),[p,C]=r.useState(""),[y,M]=r.useState([]),[$,u]=r.useState(!1),[E,O]=r.useState([]),[k,_]=r.useState(!1),[b,R]=r.useState(""),[I,v]=r.useState(""),[P,N]=r.useState(!1),[L,D]=r.useState(""),[c,S]=r.useState(`http://192.168.4.1:${me()}`),[T,F]=r.useState("OGScope_xxxx"),[q,G]=r.useState(null),[ee,se]=r.useState("—"),[ae,le]=r.useState(!1),J=r.useCallback(l=>{const j=l.mode||"unknown",A=l.active_connection||"-",Z=l.wireless_interface||"wlan0",X=l.ap_ipv4||"-",ne=l.configured?s("是","Yes"):s("否","No"),ce=l.message?`${s(",消息: ",", message: ")}${l.message}`:"";l.ap_url_hint&&S(l.ap_url_hint),l.ap_ssid&&F(l.ap_ssid);const oe=me();if(l.mdns_hostname_hint){const de=`http://${l.mdns_hostname_hint}:${oe}/debug`;G(de),se(de)}else if(l.device_id_suffix){const de=`http://${`ogscope-${l.device_id_suffix}.local`}:${oe}/debug`;G(de),se(de)}else G(null),se(s("未提供","Not provided"));g(`${s("模式","Mode")}: ${j} | ${s("活动连接","Active")}: ${A} | ${s("接口","Interface")}: ${Z} | ${s("AP地址","AP address")}: ${X} | ${s("已配置","Configured")}: ${ne}${ce}`)},[s]),Q=r.useCallback(async()=>{const l=await K("/api/network/wifi",{cache:"no-store"});J(l)},[J]);r.useEffect(()=>{Q().catch(l=>g(`${s("获取状态失败","Status request failed")}: ${l.message}`))},[Q,s]),r.useEffect(()=>{C(""),f(""),D("")},[t]);const re=async l=>{if(!window.confirm(s(`切换到 ${l.toUpperCase()} 可能中断当前连接。确认继续?`,`Switching to ${l.toUpperCase()} may disconnect this session. Continue?`)))return;g(`${s("正在切换到","Switching to")} ${l.toUpperCase()}...`);const j=await K("/api/network/wifi",{method:"POST",body:JSON.stringify({mode:l})});J(j)},x=async()=>{u(!0),C(s("扫描中...","Scanning...")),M([]);try{const l=await K("/api/network/wifi/scan",{cache:"no-store"}),j=l.networks||[],A=l.hint?` ${l.hint}`:"";M(j),C(`${s("扫描到","Found")} ${j.length} ${s("个网络","network(s)")}${A}`.trim())}catch(l){C(`${s("扫描失败","Scan failed")}: ${l instanceof Error?l.message:String(l)}`)}finally{u(!1)}},z=async l=>{const j=window.prompt(`${s("输入密码","Enter password")}: ${l}`,"");if(j!==null){f(""),g(s("正在连接...","Connecting...")),N(!0);try{const A=await K("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:l,password:j||null})});J(A);const Z=A.mode||"unknown";f(Z==="sta"?`${s("连接成功,当前连接","Connected")}: ${A.active_connection||"—"}`:`${s("连接请求已提交,当前模式","Connection submitted; mode")}: ${Z}, ${s("连接","connection")}: ${A.active_connection||"—"}`),window.setTimeout(()=>void Q().catch(()=>{}),2500)}catch(A){g(`${s("连接失败","Connection failed")}: ${A instanceof Error?A.message:String(A)}`),f(`${s("错误详情","Details")}: ${A instanceof Error?A.message:String(A)}`)}finally{N(!1)}}},Y=async()=>{const l=b.trim();if(!l){window.alert(s("请输入 SSID","Enter an SSID"));return}f(""),g(s("正在连接...","Connecting...")),N(!0);try{const j=await K("/api/network/wifi/sta/connect",{method:"POST",body:JSON.stringify({ssid:l,password:I||null})});J(j);const A=j.mode||"unknown";f(A==="sta"?`${s("连接成功,当前连接","Connected")}: ${j.active_connection||"—"}`:`${s("连接请求已提交,当前模式","Connection submitted; mode")}: ${A}, ${s("连接","connection")}: ${j.active_connection||"—"}`),window.setTimeout(()=>void Q().catch(()=>{}),2500)}catch(j){g(`${s("连接失败","Connection failed")}: ${j instanceof Error?j.message:String(j)}`),f(`${s("错误详情","Details")}: ${j instanceof Error?j.message:String(j)}`)}finally{N(!1)}},o=async()=>{_(!0);try{const l=await K("/api/network/wifi/profiles",{cache:"no-store"});O(l.profiles||[])}catch(l){O([]),window.alert(l instanceof Error?l.message:String(l))}finally{_(!1)}},m=async l=>{try{await K("/api/network/wifi/profile/activate",{method:"POST",body:JSON.stringify({connection_name:l})}),g(s("已发送激活请求","Activation requested"))}catch(j){window.alert(j instanceof Error?j.message:String(j))}};async function w(l,j){const A=`http://${l}:${j}/health`;try{const Z=new AbortController,X=window.setTimeout(()=>Z.abort(),700),ne=await fetch(A,{signal:Z.signal,mode:"cors"});if(window.clearTimeout(X),!ne.ok)return null;const ce=await ne.json();if(ce&&ce.status==="healthy")return`http://${l}:${j}`}catch{}return null}const H=async()=>{const l=me();D(s("扫描中...","Scanning..."));const j=["192.168.0","192.168.1","192.168.31","10.0.0"];for(const A of j){const Z=[];for(let X=1;X<255;X++)Z.push(`${A}.${X}`);for(let X=0;Xw(pe,l)))).find(Boolean);if(oe){D(`${s("已找到设备","Device found")}: ${oe}`),window.location.href=`${oe}/debug`;return}}}D(s("未找到设备","No device found"))},ue=r.useMemo(()=>{if(!a)return[];const l=a.wifi_quality!=null&&!Number.isNaN(Number(a.wifi_quality))?`${Number(a.wifi_quality).toFixed(1)}%`:"—",j=a.wifi_signal_dbm!=null&&!Number.isNaN(Number(a.wifi_signal_dbm))?`${Number(a.wifi_signal_dbm).toFixed(0)} dBm (${String(a.wifi_interface??"?")})`:"—";return[[s("平台","Platform"),String(a.platform??"—")],[s("系统","System"),String(a.os??"—")],[s("CPU 占用","CPU usage"),a.cpu_usage==null?"—":`${Number(a.cpu_usage).toFixed(1)}%`],[s("内存占用","Memory usage"),a.memory_usage==null?"—":`${Number(a.memory_usage).toFixed(1)}%`],[s("CPU 温度","CPU temperature"),a.temperature==null?"—":`${Number(a.temperature).toFixed(1)} °C`],[s("运行时长","Uptime"),ns(a.uptime_seconds,i)],[s("1 分钟负载","Load (1m)"),String(a.load_average_1m??"—")],[s("WiFi 质量","WiFi quality"),l],[s("WiFi 信号","WiFi signal"),j]]},[a,i,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:s("WiFi 管理、模式切换、发现与恢复工具","WiFi management, mode switching, discovery, and recovery tools")})]}),n&&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:n}),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(je,{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 re("sta").catch(l=>g(String(l))),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 re("ap").catch(l=>g(String(l))),children:"AP"}),e.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 p-1.5 hover:border-primary",onClick:()=>void Q().catch(l=>g(String(l))),children:e.jsx(ve,{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:d}),e.jsxs("div",{className:"grid gap-2 text-xs text-on-surface-variant md:grid-cols-2",children:[e.jsxs("p",{children:[s("AP 地址:","AP address: "),e.jsx("span",{className:"break-all font-mono text-on-surface",children:c})]}),e.jsxs("p",{children:[s("mDNS:","mDNS: "),q?e.jsx("a",{className:"font-mono text-primary underline",href:q,children:ee}):e.jsx("span",{className:"font-mono text-on-surface",children:ee})]})]})]})]}),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:$,className:"rounded bg-primary-container px-3 py-1.5 text-xs font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void x(),children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Pe,{className:"h-3.5 w-3.5"})," ",s("扫描 WiFi","Scan WiFi")]})})]}),e.jsx("p",{className:"mb-3 text-xs text-on-surface-variant",children:p||s("由设备端 NetworkManager 执行扫描","Scanning is performed by NetworkManager on the device")}),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:s("信号","Signal")}),e.jsx("th",{className:"p-2",children:s("安全","Security")}),e.jsx("th",{className:"p-2 text-right",children:s("操作","Action")})]})}),e.jsx("tbody",{children:y.map(l=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2 font-mono",children:l.ssid}),e.jsx("td",{className:"p-2",children:l.signal??"—"}),e.jsx("td",{className:"p-2",children:l.security??"—"}),e.jsx("td",{className:"p-2 text-right",children:e.jsx("button",{type:"button",disabled:P,className:"rounded border border-primary/40 px-2 py-1 text-xs text-primary disabled:opacity-50",onClick:()=>void z(l.ssid),children:"Connect"})})]},l.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:s("已保存网络","Known Networks")}),e.jsx("button",{type:"button",disabled:k,className:"rounded border border-outline-variant/40 px-3 py-1.5 text-xs disabled:opacity-50",onClick:()=>void o(),children:s("刷新已保存网络","Refresh saved networks")}),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:s("连接名","Connection")}),e.jsx("th",{className:"p-2",children:"SSID"}),e.jsx("th",{className:"p-2",children:s("自动连接","Auto-connect")}),e.jsx("th",{className:"p-2 text-right",children:s("操作","Action")})]})}),e.jsx("tbody",{children:E.map(l=>e.jsxs("tr",{className:"border-b border-outline-variant/10",children:[e.jsx("td",{className:"p-2",children:l.connection_name}),e.jsx("td",{className:"p-2",children:l.ssid}),e.jsx("td",{className:"p-2",children:l.autoconnect?s("是","Yes"):s("否","No")}),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 m(l.connection_name),children:"Activate"})})]},l.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:s("系统监控","System Monitor")}),e.jsx("div",{className:"grid gap-2",children:ue.map(([l,j])=>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:l}),e.jsx("span",{className:"font-mono text-on-surface",children:j})]},l))})]}),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:s("手动连接","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:b,onChange:l=>R(l.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:I,onChange:l=>v(l.target.value)})]}),e.jsx("button",{type:"button",disabled:P,className:"w-full rounded bg-primary-container px-4 py-2 text-sm font-medium text-on-primary-container disabled:opacity-50",onClick:()=>void Y(),children:s("连接并切换 STA","Connect and switch to STA")})]}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:h})]}),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:s("WiFi 引导","WiFi Setup Guide")}),e.jsxs("ol",{className:"list-decimal space-y-1 pl-4 text-xs text-on-surface-variant",children:[e.jsxs("li",{children:[s("连接热点","Join hotspot")," ",e.jsx("strong",{children:T}),s(",密码",", password")," "," ",e.jsx("code",{children:ae?"ogscopeadmin":"••••••••"})," ",e.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-1.5 py-0.5 text-[10px]",onClick:()=>le(l=>!l),children:ae?s("隐藏","Hide"):s("显示","Show")})]}),e.jsxs("li",{children:[s("浏览器打开","Open in a browser")," ",e.jsxs("span",{className:"break-all font-mono",children:["http://192.168.4.1:",me()]})]}),e.jsx("li",{children:s("扫描 WiFi 或手动填写 SSID 连接","Scan WiFi or enter an SSID manually")})]})]}),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:s("扫描常见网段并探测 /health","Scan common subnets and probe /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 H(),children:s("扫描局域网","Scan LAN")}),e.jsx("p",{className:"mt-2 text-xs text-on-surface-variant",children:L})]})]})]})]})}function is(t){var a,n;const i=(n=(a=t==null?void 0:t.data)==null?void 0:a.services)==null?void 0:n.hmi;if(!i||typeof i!="object")return null;const s=i.display;return!s||typeof s!="object"?null:s}function ls(t){try{return JSON.stringify(t,null,2)}catch{return String(t)}}function cs(){var v,P;const{t}=V(),[i,s]=r.useState(null),[a,n]=r.useState(null),[d,g]=r.useState(!1),[h,f]=r.useState(null),[p,C]=r.useState(null),[y,M]=r.useState(40),[$,u]=r.useState(80),[E,O]=r.useState(200),k=r.useCallback(async()=>{try{n(null);const N=await Qe();s(N)}catch(N){s(null),n(N instanceof Error?N.message:String(N))}},[]);r.useEffect(()=>{k()},[k]);const _=r.useCallback(async(N,L={})=>{var D;g(!0),C(null);try{const c=await es({target:"hmi",action:N,payload:L,timeout_ms:8e3});if(f(c),!c.success){const F=((D=c.error)==null?void 0:D.message)??"RPC failed";C(F);return}const S=c.data,T=S==null?void 0:S.result;T&&T.accepted===!1&&T.message?C(T.message):C(null),await k()}catch(c){f(null),C(c instanceof Error?c.message:String(c))}finally{g(!1)}},[k]),b=is(i),R=(P=(v=i==null?void 0:i.data)==null?void 0:v.services)==null?void 0:P.hmi,I=typeof(R==null?void 0:R.screen_on)=="boolean"?R.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 k(),disabled:d,children:t("sys.hmi.status.refresh")})]}),a?e.jsx("p",{className:"mt-2 text-sm text-error",children:a}):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:(b==null?void 0:b.enabled)===!0?"true":"false"})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.spidev")}),e.jsx("dd",{className:b!=null&&b.spidev_present?"text-primary":"text-error",children:b!=null&&b.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:[(b==null?void 0:b.width)??"—"," × ",(b==null?void 0:b.height)??"—"," · DC GPIO ",(b==null?void 0:b.dc_pin)??"—"]})]}),e.jsxs("div",{children:[e.jsx("dt",{children:t("sys.hmi.status.driver")}),e.jsx("dd",{children:b!=null&&b.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:I===void 0?"—":t(I?"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:(b==null?void 0:b.last_pattern)??"—"})]}),b!=null&&b.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:b.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 _("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 _("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:y,onChange:N=>M(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:$,onChange:N=>u(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:E,onChange:N=>O(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 _("display.test_pattern",{pattern:"fill",r:y,g:$,b:E}),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 _("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 _("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 _("display.release"),children:t("sys.hmi.actions.release")})]}),p?e.jsx("p",{className:"mt-3 text-sm text-error",children:p}):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:h?ls(h):"—"})]})]})]})}const ds={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 Se({scope:t}){const{t:i}=V(),s=ds[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:i("sys.placeholder.breadcrumb")}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:i(s.titleKey)}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:i(s.descKey)})]}),e.jsx("section",{className:"grid grid-cols-12 gap-4",children:s.blocks.map(a=>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:i("sys.placeholder.block")}),e.jsx("h3",{className:"mt-2 text-lg font-semibold",children:i(a)}),e.jsx("p",{className:"mt-2 text-sm text-on-surface-variant",children:i("sys.placeholder.desc")})]},a))}),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:i("sys.placeholder.status")})})]})}const B=62,ie=4;function xs(t){const{rollDeg:i,pitchDeg:s,yawRateDps:a}=t,{t:n}=V(),d=`rotateX(${-s}deg) rotateY(${i}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:n("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:n("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:ie,left:0,top:-ie/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:n("sys.sensors.gyro.axisXLabel")}),e.jsx("div",{className:"absolute bg-sky-400 shadow-md ring-1 ring-sky-200/30",style:{width:B,height:ie,left:0,top:-ie/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:n("sys.sensors.gyro.axisYLabel")}),e.jsx("div",{className:"absolute bg-violet-400 shadow-md ring-1 ring-violet-200/35",style:{width:B,height:ie,left:0,top:-ie/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:n("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:n("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:n("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:n("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:n("sys.sensors.gyro.roll")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[i.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:n("sys.sensors.gyro.pitch")}),e.jsxs("p",{className:"mt-1 text-lg font-bold tabular-nums text-teal-200",children:[s.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:n("sys.sensors.gyro.yawRate")}),e.jsx("p",{className:"mt-1 text-lg font-bold tabular-nums text-amber-200/95",children:a.toFixed(2)}),e.jsx("p",{className:"text-[9px] text-on-surface-variant",children:"°/s"})]})]})]})]})}const fe=250;function be(t){const{label:i,value:s,maxAbs:a,unit:n}=t,d=Math.max(-1,Math.min(1,s/a)),g=d>=0?50:50+d*50,h=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:i}),e.jsxs("span",{className:"font-mono text-xs tabular-nums text-sky-100",children:[s.toFixed(2),e.jsxs("span",{className:"text-on-surface-variant",children:[" ",n]})]})]}),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:`${g}%`,width:`${Math.max(h,d===0?0:.8)}%`}})]})]})}function us(t){const{bus:i,addr:s}=t,{t:a}=V(),[n,d]=r.useState(!1),[g,h]=r.useState(null),[f,p]=r.useState(null),[C,y]=r.useState(null),[M,$]=r.useState(null),[u,E]=r.useState(null),[O,k]=r.useState(null),[_,b]=r.useState(!1),R=r.useRef(null),I=r.useCallback(async()=>{var P;d(!0),h(null);try{const N=new URLSearchParams({bus:String(i),addr:String(s)}),L=await W(`/api/debug/sensors/mpu6050/imu-sample?${N.toString()}`);if(!L.success){p(null),y(null),$(null),E(null),k(null),h(((P=L.sample)==null?void 0:P.error)||L.error||a("sys.sensors.gyro.errUnknown"));return}p(L.gyro_dps??null),y(L.gyro_raw??null),$(L.tilt_deg??null),E(L.yaw_rate_dps??null),k(L.accel_g??null)}catch(N){p(null),y(null),$(null),E(null),k(null),h(N instanceof Error?N.message:String(N))}finally{d(!1)}},[s,i,a]);r.useEffect(()=>{if(!_){R.current&&(clearInterval(R.current),R.current=null);return}return R.current=setInterval(()=>void I(),500),()=>{R.current&&clearInterval(R.current)}},[_,I]);const v=M!=null&&u!=null&&Number.isFinite(M.roll)&&Number.isFinite(M.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:a("sys.sensors.gyro.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:a("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:_,onChange:P=>b(P.target.checked)}),a("sys.sensors.gyro.live")]}),e.jsx("button",{type:"button",disabled:n,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 I(),children:a(n?"sys.sensors.gyro.loading":"sys.sensors.gyro.btn")})]})]}),v&&e.jsx(xs,{rollDeg:M.roll,pitchDeg:M.pitch,yawRateDps:u}),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:a("sys.sensors.gyro.barsTitle")}),e.jsx(be,{label:"X",value:f.x,maxAbs:fe,unit:"°/s"}),e.jsx(be,{label:"Y",value:f.y,maxAbs:fe,unit:"°/s"}),e.jsx(be,{label:"Z",value:f.z,maxAbs:fe,unit:"°/s"})]}),O&&e.jsxs("p",{className:"mt-3 text-center font-mono text-[10px] text-on-surface-variant",children:["g — X:",O.x.toFixed(3)," Y:",O.y.toFixed(3)," Z:",O.z.toFixed(3)]}),C&&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:a("sys.sensors.gyro.rawBlock")}),e.jsxs("p",{className:"mt-1 font-mono text-[11px] tabular-nums text-on-surface",children:["raw X=",C.x," · Y=",C.y," · Z=",C.z]})]}),!f&&!g&&!n&&e.jsx("p",{className:"mt-4 text-[11px] text-on-surface-variant",children:a("sys.sensors.gyro.hint")}),g&&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:g})]})}const Ne=3.4,Le=7,ms=3;function ps(t){const i=(t%360+360)%360;return i<1||i>359?"cardN":Math.abs(i-90)<1?"cardE":Math.abs(i-180)<1?"cardS":Math.abs(i-270)<1?"cardW":null}function hs(){const t=[],i=Le*360;for(let s=0;s<=i;s+=5){const a=s%360,n=s%30===0,d=!n&&s%10===0,g=ps(a),h=!g&&n&&a%90!==0?a:void 0;t.push({x:s*Ne,deg:s,h:n?"maj":d?"mid":"min",degLabel:h,cardKey:g??void 0})}return t}const fs=hs(),Ce=Le*360*Ne;function bs(t,i){const s=(t%360+360)%360;let n=(i%360+360)%360-s;return n=(n+180)%360-180,t+n}function gs(t){const{bus:i,addr:s}=t,{t:a}=V(),[n,d]=r.useState(!1),[g,h]=r.useState(null),[f,p]=r.useState(null),[C,y]=r.useState(null),[M,$]=r.useState(!1),u=r.useRef(null),E=r.useRef(0),O=r.useRef(null),[k,_]=r.useState(320);r.useEffect(()=>{const v=O.current;if(!v)return;const P=new ResizeObserver(()=>{_(Math.max(200,v.clientWidth))});return P.observe(v),_(Math.max(200,v.clientWidth)),()=>P.disconnect()},[]),r.useEffect(()=>{p(null),y(null),h(null)},[i,s]);const b=r.useCallback(async()=>{const v=++E.current;d(!0);try{const P=new URLSearchParams({bus:String(i),addr:String(s)}),N=await W(`/api/debug/sensors/magnetometer/sample?${P.toString()}`);if(v!==E.current)return;if(!N.success){p(null),y(null),h(N.error||a("sys.sensors.compass.err"));return}h(null);const L=N.heading_deg??null;L!=null&&p(D=>D==null?ms*360+L:bs(D,L)),y(N.field_ut??null)}catch(P){if(v!==E.current)return;p(null),y(null),h(P instanceof Error?P.message:String(P))}finally{v===E.current&&d(!1)}},[s,i,a]);r.useEffect(()=>{if(!M){u.current&&(clearInterval(u.current),u.current=null);return}return u.current=setInterval(()=>{b()},850),()=>{u.current&&clearInterval(u.current)}},[M,b]);const R=r.useMemo(()=>{if(f==null)return 0;const v=f*Ne;return k/2-v},[f,k]),I=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:a("sys.sensors.compass.title")}),e.jsx("p",{className:"mt-1 max-w-xl text-[11px] leading-snug text-on-surface-variant",children:a("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:M,onChange:v=>$(v.target.checked)}),a("sys.sensors.compass.live")]}),e.jsx("button",{type:"button",disabled:n,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 b(),children:a(n?"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:a("sys.sensors.compass.tapeCaption")}),e.jsxs("div",{ref:O,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:Ce,transform:`translateX(${R}px)`,transition:"transform 0.42s cubic-bezier(0.22, 0.95, 0.28, 1)"},children:e.jsxs("div",{className:"relative h-full",style:{width:Ce,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"}),fs.map(v=>{const P=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?a(`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:P}})]},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:a("sys.sensors.compass.headingLabel")}),e.jsx("p",{className:"font-mono text-4xl font-bold tabular-nums text-sky-200",children:I!=null?`${I.toFixed(1)}°`:"—"}),e.jsx("p",{className:"mt-1 text-[10px] text-on-surface-variant",children:a("sys.sensors.compass.headingHint")})]}),C&&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:[C.x.toFixed(2)," · ",C.y.toFixed(2)," · ",C.z.toFixed(2)]})]})]}),g&&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:g}),e.jsx("p",{className:"mt-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:a("sys.sensors.compass.footnote")})]})}function $e(t){try{return JSON.stringify(t,null,2)}catch{return String(t)}}function ys(){const{t,locale:i}=V(),[s,a]=r.useState(1),[n,d]=r.useState(12),[g,h]=r.useState(!0),[f,p]=r.useState(!1),[C,y]=r.useState(null),[M,$]=r.useState(null),[u,E]=r.useState(null),[O,k]=r.useState(null),[_,b]=r.useState(1),[R,I]=r.useState(104),[v,P]=r.useState(!0),[N,L]=r.useState(!1),[D,c]=r.useState(null),[S,T]=r.useState(null),F=r.useCallback(async()=>{p(!0),y(null);try{const x=new URLSearchParams({bus:String(s),addr:String(n),i2cdetect:g?"true":"false"}),z=await W(`/api/debug/sensors/magnetometer/selftest?${x.toString()}`);$(z)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}},[n,s,g]),q=r.useCallback(async()=>{p(!0),y(null);try{const x=new URLSearchParams({addr:String(n)}),z=await W(`/api/debug/sensors/magnetometer/probe-buses?${x.toString()}`);$(z)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}},[n]),G=r.useCallback(async()=>{p(!0),y(null);try{const x=new URLSearchParams({bus:String(s),addr:String(n)}),z=await W(`/api/debug/sensors/magnetometer/calibration/start?${x.toString()}`,{method:"POST"});$(z),k(i==="zh"?"已开始方向校准,请缓慢旋转设备 5-15 秒。":"Heading calibration started. Rotate the device slowly for 5–15 seconds.");const Y=await W(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);E(Y)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}},[n,s,i]),ee=r.useCallback(async()=>{p(!0),y(null);try{const x=new URLSearchParams({bus:String(s),addr:String(n)}),z=await W(`/api/debug/sensors/magnetometer/calibration/commit?${x.toString()}`,{method:"POST"});$(z),k(i==="zh"?"已保存并锁定方向校准。":"Heading calibration saved and locked.");const Y=await W(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);E(Y)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}},[n,s,i]),se=r.useCallback(async()=>{if(window.confirm(i==="zh"?"重置已保存的方向校准并恢复自动模式?":"Reset the saved heading calibration and return to auto mode?")){p(!0),y(null);try{const x=new URLSearchParams({bus:String(s),addr:String(n)}),z=await W(`/api/debug/sensors/magnetometer/calibration/reset?${x.toString()}`,{method:"POST"});$(z),k(i==="zh"?"已重置到自动模式。":"Calibration reset to auto mode.");const Y=await W(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);E(Y)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}}},[n,s,i]),ae=r.useCallback(async()=>{p(!0),y(null);try{const x=new URLSearchParams({bus:String(s),addr:String(n)}),z=await W(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`);$(z),E(z)}catch(x){$(null),y(x instanceof Error?x.message:String(x))}finally{p(!1)}},[n,s]);r.useEffect(()=>{const x=new URLSearchParams({bus:String(s),addr:String(n)});W(`/api/debug/sensors/magnetometer/calibration/status?${x.toString()}`).then(z=>E(z)).catch(()=>{})},[n,s]),r.useEffect(()=>{if((u==null?void 0:u.mode)!=="recording")return;const x=setInterval(()=>{const z=new URLSearchParams({bus:String(s),addr:String(n)});W(`/api/debug/sensors/magnetometer/calibration/status?${z.toString()}`).then(Y=>E(Y)).catch(()=>{})},900);return()=>clearInterval(x)},[u==null?void 0:u.mode,s,n]);const le=r.useMemo(()=>{const x=(u==null?void 0:u.mode)??"auto";return t(x==="recording"?"sys.sensors.mag.calModeRecording":x==="locked"?"sys.sensors.mag.calModeLocked":"sys.sensors.mag.calModeAuto")},[u==null?void 0:u.mode,t]),J=Number((u==null?void 0:u.samples)??0),Q=(u==null?void 0:u.mode)==="recording"&&J>=10,re=r.useCallback(async()=>{L(!0),c(null);try{const x=new URLSearchParams({bus:String(_),addr:String(R),i2cdetect:v?"true":"false"}),z=await W(`/api/debug/sensors/mpu6050/selftest?${x.toString()}`);T(z)}catch(x){T(null),c(x instanceof Error?x.message:String(x))}finally{L(!1)}},[R,_,v]);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:s,onChange:x=>a(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:n,onChange:x=>d(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:g,onChange:x=>h(x.target.checked)}),t("sys.sensors.mag.i2cdetect")]})]}),e.jsx(gs,{bus:s,addr:n}),e.jsxs("div",{className:"mt-6 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:f,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void F(),children:t(f?"sys.sensors.mag.running":"sys.sensors.mag.btnSelftest")}),e.jsx("button",{type:"button",disabled:f,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 q(),children:t("sys.sensors.mag.btnProbe")})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsx("button",{type:"button",disabled:f||(u==null?void 0:u.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 G(),children:t("sys.sensors.mag.btnCalStart")}),e.jsx("button",{type:"button",disabled:f||!Q,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 ee(),children:t("sys.sensors.mag.btnCalCommit")}),e.jsx("button",{type:"button",disabled:f,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 se(),children:t("sys.sensors.mag.btnCalReset")}),e.jsx("button",{type:"button",disabled:f,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 ae(),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")," ",le]}),e.jsxs("p",{className:"mt-1 text-on-surface-variant",children:[t("sys.sensors.mag.calSamplesPrefix")," ",J,(u==null?void 0:u.mode)==="recording"?" / 10+":""]}),(u==null?void 0:u.span_xyz)&&e.jsxs("p",{className:"mt-1 font-mono text-[10px] text-on-surface-variant",children:["span xyz: ",Number(u.span_xyz.x??0).toFixed(1)," /"," ",Number(u.span_xyz.y??0).toFixed(1)," /"," ",Number(u.span_xyz.z??0).toFixed(1)]}),(u==null?void 0:u.mode)==="recording"&&e.jsx("p",{className:"mt-1 text-amber-200",children:t("sys.sensors.mag.calRecordingHint")}),(u==null?void 0:u.mode)==="locked"&&u.locked&&e.jsxs("p",{className:"mt-1 text-emerald-200",children:[t("sys.sensors.mag.calLockedHint")," axes=",String(u.locked.axes_pair??"xy")]}),O&&e.jsx("p",{className:"mt-1 text-sky-200",children:O})]}),C&&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:C}),M&&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:$e(M)})]})]}),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:_,onChange:x=>b(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:R,onChange:x=>I(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:v,onChange:x=>P(x.target.checked)}),t("sys.sensors.mag.i2cdetect")]})]}),e.jsx(us,{bus:_,addr:R}),e.jsx("div",{className:"mt-4 flex flex-wrap gap-2",children:e.jsx("button",{type:"button",disabled:N,className:"rounded-lg bg-primary px-4 py-2 text-sm font-medium text-on-primary hover:opacity-90 disabled:opacity-50",onClick:()=>void re(),children:t(N?"sys.sensors.mpu.running":"sys.sensors.mpu.btnSelftest")})}),D&&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:D}),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: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:$e(S)})]})]})]})}const vs={ogscope:{zh:"主配置 ogscope.env",en:"Primary ogscope.env"},network:{zh:"网络 network.env",en:"Network network.env"}};function js(t){const i=[];let s=0;return t.split(/\r?\n/).forEach((n,d)=>{const g=n.trim();if(!g||g.startsWith("#"))return;const h=g.startsWith("export ")?g.slice(7):g,f=h.indexOf("=");if(f<=0){s+=1;return}const p=h.slice(0,f).trim();if(!p){s+=1;return}const C=h.slice(f+1);i.push({id:`env-${d}-${p}`,key:p,value:C})}),{entries:i,unsupportedLines:s}}function _e(t){const i=t.map(s=>({key:s.key.trim(),value:s.value})).filter(s=>s.key.length>0).map(s=>`${s.key}=${s.value}`);return i.length>0?`${i.join(` +`)} +`:""}function xe(t,i){return t==="both"?!0:t===i}function Ns(){const{locale:t}=V(),[i,s]=r.useState([]),[a,n]=r.useState(null),[d,g]=r.useState(""),[h,f]=r.useState(""),[p,C]=r.useState("form"),[y,M]=r.useState([]),[$,u]=r.useState(0),[E,O]=r.useState(!1),[k,_]=r.useState(""),[b,R]=r.useState(""),[I,v]=r.useState(""),[P,N]=r.useState(!0),[L,D]=r.useState(""),c=t==="zh",S=r.useMemo(()=>i.find(o=>o.file_id===d)??null,[d,i]),T=r.useMemo(()=>{const o=new Map;for(const m of(a==null?void 0:a.sections)??[])for(const w of m.entries)o.set(w.key.toUpperCase(),w);for(const m of(a==null?void 0:a.network_only)??[])o.set(m.key.toUpperCase(),m);return o},[a]),F=r.useMemo(()=>{if(!d)return[];const o=new Set(y.map(w=>w.key.trim().toUpperCase()).filter(Boolean)),m=[];for(const w of(a==null?void 0:a.sections)??[])for(const H of w.entries)xe(H.scope,d)&&(o.has(H.key.toUpperCase())||m.push(H));for(const w of(a==null?void 0:a.network_only)??[])xe(w.scope,d)&&(o.has(w.key.toUpperCase())||m.push(w));return m.sort((w,H)=>w.key.localeCompare(H.key))},[d,a,y]),q=r.useMemo(()=>{const o=I.trim().toLowerCase();return o?y.filter(m=>{const w=m.key.toLowerCase(),H=T.get(m.key.trim().toUpperCase());return`${w} ${m.value} ${(H==null?void 0:H.zh)??""} ${(H==null?void 0:H.en)??""}`.toLowerCase().includes(o)}):y},[y,T,I]),G=o=>{const m=js(o);M(m.entries),u(m.unsupportedLines)},ee=async()=>{var o,m;O(!0),_("");try{const[w,H]=await Promise.all([K("/api/dev/system/config/files",{cache:"no-store"}),K("/api/dev/system/config/catalog",{cache:"no-store"})]);s(w.files??[]),n(H);const ue=((m=(o=w.files)==null?void 0:o[0])==null?void 0:m.file_id)??"";g(l=>{var j;return l&&((j=w.files)!=null&&j.some(A=>A.file_id===l))?l:ue})}catch(w){_(w instanceof Error?w.message:String(w))}finally{O(!1)}},se=async()=>{if(!d)return;if(p==="form"&&$>0){_(c?"当前文件包含无法表单化的行,请切换到「原始文本」模式编辑后再保存。":"This file has lines not supported by form mode. Switch to Raw mode before saving.");return}const o=p==="form"?_e(y):h;O(!0),_(""),R("");try{const m=await K("/api/dev/system/config/files",{method:"POST",body:JSON.stringify({file_id:d,content:o})});R(c?`保存成功:${m.message??d};请重启 ogscope 服务使配置生效`:`Saved: ${m.message??d}; restart ogscope to apply changes`),await ee()}catch(m){_(m instanceof Error?m.message:String(m))}finally{O(!1)}};r.useEffect(()=>{ee()},[]),r.useEffect(()=>{if(!S){f(""),M([]),u(0),D("");return}const o=S.content??"";f(o),G(o),D("")},[S]);const ae=()=>{M(o=>[...o,{id:`env-new-${Date.now()}-${o.length}`,key:"",value:""}])},le=()=>{const o=F.find(m=>m.key===L);o&&(M(m=>[...m,{id:`env-catalog-${Date.now()}-${o.key}`,key:o.key,value:o.default??""}]),D(""))},J=(o,m)=>{M(w=>w.map(H=>H.id===o?{...H,...m}:H))},Q=o=>{M(m=>m.filter(w=>w.id!==o))},re=o=>{const m=T.get(o.trim().toUpperCase());return m?c?m.zh:m.en:c?"暂无释义(可查阅 deploy/*.env.example)":"No hint (see deploy/*.env.example)"},x=o=>{const m=T.get(o.trim().toUpperCase());return m!=null&&m.default?c?`默认:${m.default}`:`Default: ${m.default}`:""},z=o=>{const m=vs[o];return m?c?m.zh:m.en:o},Y=o=>o.writable?o.writable_via_sudo&&!o.writable_direct?c?"可写(经 sudo 助手)":"Writable (via sudo helper)":o.writable_direct?c?"可写(直接)":"Writable (direct)":c?"可写":"Writable":c?"不可写:请运行 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:c?"配置管理":"Config Manager"})]}),e.jsx("h2",{className:"mt-1 font-headline text-3xl font-black tracking-tight",children:c?"环境配置管理":"Environment Config Manager"}),e.jsx("p",{className:"text-sm text-on-surface-variant",children:c?"编辑 /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."})]}),k&&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:k}),b&&e.jsx("div",{className:"rounded-lg border border-primary/30 bg-primary/10 px-3 py-2 text-sm",children:b}),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:c?"配置文件":"Config Files"}),e.jsx("button",{type:"button",onClick:()=>void ee(),disabled:E,children:e.jsxs("span",{className:"inline-flex items-center gap-1 text-xs",children:[e.jsx(ve,{className:"h-3.5 w-3.5"})," ",c?"刷新":"Refresh"]})})]}),i.map(o=>e.jsxs("button",{type:"button",onClick:()=>g(o.file_id),className:`w-full rounded-lg border px-3 py-2 text-left text-sm ${o.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:z(o.file_id)}),e.jsx("div",{className:"mt-1 truncate font-mono text-[11px]",children:o.path})]},o.file_id)),(a==null?void 0:a.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:c?"配置路径":"Config paths"}),Object.entries(a.env_files).map(([o,m])=>e.jsxs("p",{className:"font-mono",children:[o,": ",m]},o))]})]}),e.jsxs("div",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4 lg:col-span-9",children:[!S&&e.jsx("p",{className:"text-sm text-on-surface-variant",children:c?"暂无可编辑配置文件":"No editable config files."}),S&&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:S.path}),e.jsxs("p",{className:"text-xs text-on-surface-variant",children:[Y(S)," · ",c?"存在":"Exists",": ",String(S.exists)]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("button",{type:"button",onClick:()=>{p==="raw"&&G(h),C("form")},className:`rounded px-2 py-1 text-xs ${p==="form"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:c?"表单模式":"Form"}),e.jsx("button",{type:"button",onClick:()=>{p==="form"&&f(_e(y)),C("raw")},className:`rounded px-2 py-1 text-xs ${p==="raw"?"bg-primary-container text-on-primary-container":"border border-outline-variant/30 text-on-surface-variant"}`,children:c?"原始文本":"Raw"})]}),e.jsx("button",{type:"button",onClick:()=>void se(),disabled:E||!S.writable,children:e.jsxs("span",{className:"inline-flex items-center gap-1",children:[e.jsx(Fe,{className:"h-3.5 w-3.5"}),c?"保存并提示重启":"Save"]})})]}),S.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:S.error}),p==="form"?e.jsxs("div",{className:"space-y-3",children:[$>0&&e.jsx("div",{className:"rounded border border-warning/40 bg-warning/10 px-2 py-1 text-xs text-on-surface",children:c?`检测到 ${$} 行无法转换为键值表单。请切到「原始文本」模式处理。`:`${$} 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(Pe,{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:c?"搜索键名、值或释义…":"Search keys, values, or hints…",value:I,onChange:o=>v(o.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:L,onChange:o=>D(o.target.value),children:[e.jsx("option",{value:"",children:c?"从目录添加配置项…":"Add from catalog…"}),F.map(o=>e.jsx("option",{value:o.key,children:o.key},o.key))]}),e.jsx("button",{type:"button",disabled:!L,onClick:le,className:"rounded border border-outline-variant/30 px-2 py-1.5 text-xs disabled:opacity-50",children:c?"添加":"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:c?"配置项":"Key"}),e.jsx("th",{children:c?"值":"Value"}),e.jsx("th",{children:c?"释义":"Meaning"}),e.jsx("th",{children:c?"操作":"Action"})]})}),e.jsx("tbody",{children:q.map(o=>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:o.key,onChange:m=>J(o.id,{key:m.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:c?"变量值":"Value",value:o.value,onChange:m=>J(o.id,{value:m.target.value})})}),e.jsxs("td",{className:"pr-2 align-top text-xs text-on-surface-variant",children:[e.jsx("p",{children:re(o.key)}),x(o.key)&&e.jsx("p",{className:"mt-0.5 font-mono text-[10px] text-on-surface-variant/80",children:x(o.key)})]}),e.jsx("td",{className:"align-top",children:e.jsx("button",{type:"button",onClick:()=>Q(o.id),children:e.jsx(Ie,{className:"h-3.5 w-3.5 text-on-surface-variant"})})})]},o.id))})]}),y.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:c?"当前没有可编辑变量,可从目录添加或手动新增。":"No variables yet. Add from catalog or manually."}),y.length>0&&q.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:c?"无匹配项,请调整搜索条件。":"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(Xe,{className:"h-3.5 w-3.5"}),c?"空白行":"Blank row"]})})}),e.jsxs("details",{open:P,onToggle:o=>N(o.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(qe,{className:"h-3.5 w-3.5"}),c?"配置目录(按模块)":"Config catalog (by section)"]})}),e.jsxs("div",{className:"mt-3 space-y-4",children:[((a==null?void 0:a.sections)??[]).map(o=>{const m=o.entries.filter(w=>d?xe(w.scope,d):!0);return m.length===0?null:e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:c?o.title_zh:o.title_en}),e.jsx("div",{className:"space-y-1",children:m.map(w=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:w.key}),w.default!=null&&w.default!==""&&e.jsxs("span",{className:"ml-1 font-mono text-[10px] text-on-surface-variant/80",children:["(= ",w.default,")"]})," — ",e.jsx("span",{children:c?w.zh:w.en})]},w.key))})]},o.id)}),((a==null?void 0:a.network_only)??[]).filter(o=>d?xe(o.scope,d):!0).length>0&&e.jsxs("div",{children:[e.jsx("p",{className:"mb-1 font-medium text-on-surface",children:c?"仅 network.env / 脚本":"network.env / scripts only"}),e.jsx("div",{className:"space-y-1",children:((a==null?void 0:a.network_only)??[]).filter(o=>d?xe(o.scope,d):!0).map(o=>e.jsxs("p",{children:[e.jsx("span",{className:"font-mono text-[11px] text-on-surface",children:o.key})," — ",e.jsx("span",{children:c?o.zh:o.en})]},o.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:h,onChange:o=>f(o.target.value)})]})]})]})]})}const ws=new Set(["overview","network","sensors","hmi","power","config"]);function Me(){const t=window.location.hash.replace(/^#\/?/,"").trim().toLowerCase();return ws.has(t)?t:"overview"}function ks(t){window.location.hash=`/${t}`}function Ss(){const[t,i]=r.useState(()=>Me()),[s,a]=r.useState(!0);r.useEffect(()=>{const d=()=>i(Me());return window.addEventListener("hashchange",d),()=>window.removeEventListener("hashchange",d)},[]),r.useEffect(()=>{(async()=>{var d;try{const g=await fetch("/api",{cache:"no-store"});if(!g.ok)return;const h=await g.json();a(!!((d=h.endpoints)!=null&&d.network))}catch{}})()},[]);const n=r.useMemo(()=>t==="network"?s?e.jsx(os,{}):e.jsx(Se,{scope:"network"}):t==="sensors"?e.jsx(ys,{}):t==="hmi"?e.jsx(cs,{}):t==="config"?e.jsx(Ns,{}):t==="power"?e.jsx(Se,{scope:"power"}):e.jsx(rs,{}),[s,t]);return e.jsx(Ve,{route:t,allowNetworkRoute:s,onRouteChange:d=>{d==="network"&&!s||d!==t&&ks(d)},children:n})}Re.createRoot(document.getElementById("root")).render(e.jsx(Oe.StrictMode,{children:e.jsx(De,{children:e.jsx(ze,{children:e.jsx(Ss,{})})})})); diff --git a/web/static/analysis-lab/assets/system-Z3O5uO1l.js b/web/static/analysis-lab/assets/system-Z3O5uO1l.js deleted file mode 100644 index 9ce3790..0000000 --- a/web/static/analysis-lab/assets/system-Z3O5uO1l.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 W,a as q,S as Pe,b as Le}from"./http-VZMNcsmS.js";import{c as H,u as Z,T as Re,I as Te}from"./index-C78KOEFu.js";import{R as he}from"./refresh-cw-gseCLMRP.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/camera.html b/web/static/analysis-lab/camera.html index 9fb1163..4dafffb 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,11 +4,11 @@ OGScope 相机调试控制台 - + - - - + + +
diff --git a/web/static/analysis-lab/index.html b/web/static/analysis-lab/index.html index 4180b35..1aee890 100644 --- a/web/static/analysis-lab/index.html +++ b/web/static/analysis-lab/index.html @@ -4,11 +4,11 @@ OGScope 星空解算控制台 - + - - - + + +
diff --git a/web/static/analysis-lab/system.html b/web/static/analysis-lab/system.html index 8e2ea22..f12f1ca 100644 --- a/web/static/analysis-lab/system.html +++ b/web/static/analysis-lab/system.html @@ -4,12 +4,12 @@ OGScope 系统调试控制台 - + - - - - + + + +
diff --git a/web/static/i18n/analysis.en.json b/web/static/i18n/analysis.en.json index 0358472..8237c67 100644 --- a/web/static/i18n/analysis.en.json +++ b/web/static/i18n/analysis.en.json @@ -522,6 +522,7 @@ "cam.notice.needPreview": "Start preview first", "cam.notice.needManualForAutoAdjust": "Switch to manual exposure before auto-adjust", "cam.notice.autoAdjusted": "Auto-adjust completed and applied", + "cam.confirm.reset": "Reset camera settings on the current device?", "cam.confirm.deletePreset": "Delete preset {name}?", "cam.confirm.deleteFile": "Delete file {name}?", "sys.sensors.mag.calStatusPrefix": "Calibration mode:", diff --git a/web/static/i18n/analysis.zh.json b/web/static/i18n/analysis.zh.json index 3e7bd78..d66535b 100644 --- a/web/static/i18n/analysis.zh.json +++ b/web/static/i18n/analysis.zh.json @@ -523,6 +523,7 @@ "cam.notice.needPreview": "请先启动预览", "cam.notice.needManualForAutoAdjust": "请先切换为手动曝光再进行智能调整", "cam.notice.autoAdjusted": "智能调整已完成并生效", + "cam.confirm.reset": "将重置当前设备的相机设置。确认继续?", "cam.confirm.deletePreset": "确定删除预设 {name} 吗?", "cam.confirm.deleteFile": "确定删除文件 {name} 吗?", "sys.sensors.mag.calStatusPrefix": "校准状态:", From 7f3167b2f507eb9c58f362bceb348c2382ce563c 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: Mon, 24 Aug 2026 20:56:21 +0800 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=E4=BA=A4=E4=BB=98=E6=98=9F?= =?UTF-8?q?=E7=82=B9=E7=84=A6=E7=82=B9=E6=A0=A1=E5=87=86=E5=B9=B6=E5=8A=A0?= =?UTF-8?q?=E5=9B=BA=E7=9B=B8=E6=9C=BA=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=20/=20deliver=20star=20focus=20calibration=20and=20harden=20ca?= =?UTF-8?q?mera=20lifecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEBUG_CONSOLE.md | 21 +- docs/DEBUG_CONSOLE_EN.md | 17 +- docs/contracts/dev-rest-v1.md | 13 + docs/contracts/dev-rest-v1_EN.md | 13 + docs/development/ogscope-service-hardening.md | 8 + ogscope/config.py | 9 + ogscope/config_catalog.py | 1 + ogscope/domain/camera/focus.py | 262 +++++++++++ ogscope/domain/camera/services.py | 21 + ogscope/platform/hardware/camera.py | 26 +- ogscope/web/api/debug/routes.py | 22 + ogscope/web/api/debug/services.py | 118 ++--- ogscope/web/camera_shared.py | 219 ++++++--- tests/unit/test_camera_flip.py | 75 ++++ tests/unit/test_camera_manager_health.py | 71 +++ tests/unit/test_debug_camera_api.py | 48 ++ tests/unit/test_focus_metrics.py | 102 +++++ web/spa/src/apps/camera/CameraConsoleApp.tsx | 416 +++++++++++++++++- ...lysis-Dd7iQ8pr.js => analysis-CpY6-RHN.js} | 25 +- .../analysis-lab/assets/camera-aU1xxh-S.js | 56 --- .../analysis-lab/assets/camera-l9M7WKlh.js | 61 +++ .../{http-lbh6zBy9.js => http-BxpNn71N.js} | 2 +- .../analysis-lab/assets/index-BtAa6NQk.css | 1 - .../analysis-lab/assets/index-CV2LaaT8.js | 26 -- .../analysis-lab/assets/index-Cm13xeJE.css | 1 + .../analysis-lab/assets/index-DfDaIsmY.js | 26 ++ ...-cw-CIJtimxg.js => refresh-cw-BwAka9-0.js} | 2 +- .../assets/rotate-ccw-B0-EEdqV.js | 6 + ...{system-BMLXYbaG.js => system-BqTKpy33.js} | 2 +- web/static/analysis-lab/camera.html | 9 +- web/static/analysis-lab/index.html | 9 +- web/static/analysis-lab/system.html | 10 +- web/static/i18n/analysis.en.json | 37 ++ web/static/i18n/analysis.zh.json | 37 ++ 34 files changed, 1532 insertions(+), 240 deletions(-) create mode 100644 ogscope/domain/camera/focus.py create mode 100644 tests/unit/test_focus_metrics.py rename web/static/analysis-lab/assets/{analysis-Dd7iQ8pr.js => analysis-CpY6-RHN.js} (96%) delete mode 100644 web/static/analysis-lab/assets/camera-aU1xxh-S.js create mode 100644 web/static/analysis-lab/assets/camera-l9M7WKlh.js rename web/static/analysis-lab/assets/{http-lbh6zBy9.js => http-BxpNn71N.js} (97%) delete mode 100644 web/static/analysis-lab/assets/index-BtAa6NQk.css delete mode 100644 web/static/analysis-lab/assets/index-CV2LaaT8.js create mode 100644 web/static/analysis-lab/assets/index-Cm13xeJE.css create mode 100644 web/static/analysis-lab/assets/index-DfDaIsmY.js rename web/static/analysis-lab/assets/{refresh-cw-CIJtimxg.js => refresh-cw-BwAka9-0.js} (91%) create mode 100644 web/static/analysis-lab/assets/rotate-ccw-B0-EEdqV.js rename web/static/analysis-lab/assets/{system-BMLXYbaG.js => system-BqTKpy33.js} (99%) diff --git a/docs/DEBUG_CONSOLE.md b/docs/DEBUG_CONSOLE.md index f5b5cbe..77955de 100644 --- a/docs/DEBUG_CONSOLE.md +++ b/docs/DEBUG_CONSOLE.md @@ -13,6 +13,13 @@ OGScope 调试控制台是一个专为开发者设计的相机调试工具,提 - 支持启动/停止预览 - 实时状态显示:采集帧率、预览帧率、曝光、消费者数量、编码器与内存压力 +### ✨ 星点焦点校准 +- 一键进入焦点校准,自动持续测量多颗可用星点的 HFD 与 FWHM +- 用“继续、接近最佳、向错误方向”等直观提示引导用户缓慢旋转镜头 +- 记录本次调焦的最佳 HFD 与变化趋势;数值越低通常越锐利 +- 可直接点击预览中的星点锁定目标,也可恢复自动多星统计 +- 自动排除饱和、信噪比不足或形状异常的星点,避免误导 + ### 📸 拍摄控制 - **单张拍摄**: 拍摄高质量照片并自动保存 - **视频录制**: 手动控制录制时长,支持MP4格式 @@ -83,23 +90,28 @@ python -m ogscope.web.app - 拖动滑块调整曝光和增益 - 点击 "应用设置" 使参数生效 -3. **拍摄照片** +3. **校准焦点(可选)** + - 点击预览区的“开始焦点校准” + - 缓慢旋转镜头,观察 HFD 数值与趋势提示 + - 让 HFD 尽可能接近本次最佳值;需要时点击画面中的星点锁定目标 + +4. **拍摄照片** - 切换到 "拍摄控制" 标签页 - 点击 "拍摄照片" 按钮 - 照片自动保存到 `~/dev_captures/` 目录 -4. **录制视频** +5. **录制视频** - 点击 "开始录制" 按钮 - 录制过程中显示计时器 - 点击 "停止录制" 结束录制 -5. **管理预设** +6. **管理预设** - 切换到 "预设管理" 标签页 - 输入预设名称和描述 - 点击 "保存预设" 保存当前设置 - 点击预设卡片上的 "应用" 快速切换 -6. **查看文件** +7. **查看文件** - 切换到 "文件管理" 标签页 - 查看所有拍摄文件 - 点击 "下载" 下载文件到本地 @@ -151,6 +163,7 @@ python -m ogscope.web.app - `POST /api/dev/debug/camera/start` - 启动相机 - `POST /api/dev/debug/camera/stop` - 停止相机 - `GET /api/dev/debug/camera/preview` - 获取预览图像 +- `GET /api/dev/debug/camera/focus/metrics` - 获取当前帧的星点焦点指标 ### 拍摄功能 - `POST /api/dev/debug/camera/capture` - 拍摄照片 diff --git a/docs/DEBUG_CONSOLE_EN.md b/docs/DEBUG_CONSOLE_EN.md index 117c826..1427a52 100644 --- a/docs/DEBUG_CONSOLE_EN.md +++ b/docs/DEBUG_CONSOLE_EN.md @@ -13,6 +13,13 @@ The OGScope debug console is a developer-focused camera tool: live preview, capt - Start/stop preview - Live status: capture FPS, preview FPS, exposure, consumers, encoder, and memory pressure +### Star focus calibration +- Start a guided focusing session and continuously measure multi-star HFD and FWHM. +- Follow plain-language guidance such as keep turning, near best, or wrong direction. +- Track the best HFD and recent trend for the current session; lower is normally sharper. +- Click a star in the preview to lock it, or return to automatic multi-star aggregation. +- Saturated, low-SNR, and badly shaped candidates are excluded to avoid misleading scores. + ### Capture - **Still capture**: high-quality photos with auto-save - **Video recording**: manual duration, MP4 @@ -68,10 +75,11 @@ Browser: `http://localhost:8000/debug` 1. **Start preview** — click Start, wait for init, view stream. 2. **Tune parameters** — Parameters tab, adjust sliders, Apply. -3. **Capture still** — Capture tab, Capture photo; files under `~/dev_captures/`. -4. **Record video** — Start recording, stop when done. -5. **Presets** — Presets tab: name, description, Save; Apply from cards. -6. **Files** — Files tab: list, download, details. +3. **Calibrate focus (optional)** — start focus calibration, rotate the lens slowly, and minimize HFD. Click a preview star when you want to lock the target. +4. **Capture still** — Capture tab, Capture photo; files under `~/dev_captures/`. +5. **Record video** — Start recording, stop when done. +6. **Presets** — Presets tab: name, description, Save; Apply from cards. +7. **Files** — Files tab: list, download, details. ### Keyboard shortcuts @@ -115,6 +123,7 @@ Browser: `http://localhost:8000/debug` - `POST /api/dev/debug/camera/start` - `POST /api/dev/debug/camera/stop` - `GET /api/dev/debug/camera/preview` +- `GET /api/dev/debug/camera/focus/metrics` ### Capture - `POST /api/dev/debug/camera/capture` diff --git a/docs/contracts/dev-rest-v1.md b/docs/contracts/dev-rest-v1.md index 1cdfa28..d5e227b 100644 --- a/docs/contracts/dev-rest-v1.md +++ b/docs/contracts/dev-rest-v1.md @@ -43,6 +43,19 @@ - `noiseReductionMode` - `previewEncoder` +### 星点焦点校准 + +- `GET /api/dev/debug/camera/focus/metrics` + - 用途:基于当前原始相机帧计算实时星点锐度,引导用户手动调整镜头焦距。 + - 可选查询参数:`target_x` 与 `target_y`,均为 `0..1` 归一化坐标;必须成对提供,用于锁定预览画面中点击的星点。 + - 响应包含: + - `aggregate.median_hfd_px` / `aggregate.hfd_mad_px`:可用星点的 HFD 中位数与离散度 + - `aggregate.median_fwhm_px`:可用星点的 FWHM 中位数 + - `aggregate.median_concentration`:星点核心能量集中度 + - `stars` / `selected_star`:候选星点及当前选择 + - `stars_detected` / `stars_measured` / `stars_used`:检测、测量与质量筛选计数 + - HFD 与 FWHM 越低通常表示越锐利;这些数值只适合在同一镜头、曝光、增益与目标区域下做相对比较,不是跨设备绝对标定值。 + ## 分析实验扩展 - `POST /api/dev/analysis/solve/frame` diff --git a/docs/contracts/dev-rest-v1_EN.md b/docs/contracts/dev-rest-v1_EN.md index 1bf304a..2dacb90 100644 --- a/docs/contracts/dev-rest-v1_EN.md +++ b/docs/contracts/dev-rest-v1_EN.md @@ -43,6 +43,19 @@ This document describes OGScope **developer-domain** APIs (internal). They are * - `noiseReductionMode` - `previewEncoder` +### Star focus calibration + +- `GET /api/dev/debug/camera/focus/metrics` + - Purpose: measure star sharpness from the current raw camera frame and guide manual lens focusing. + - Optional query parameters: `target_x` and `target_y`, both normalized to `0..1`; they must be supplied together and select the star nearest the clicked preview position. + - The response includes: + - `aggregate.median_hfd_px` / `aggregate.hfd_mad_px`: median HFD and dispersion across usable stars + - `aggregate.median_fwhm_px`: median FWHM across usable stars + - `aggregate.median_concentration`: median core-energy concentration + - `stars` / `selected_star`: candidate stars and the current selection + - `stars_detected` / `stars_measured` / `stars_used`: detection, measurement, and quality-filter counts + - Lower HFD and FWHM normally mean sharper focus. Treat the values as relative measurements within the same lens, exposure, gain, and target region, not as an absolute cross-device calibration. + ## Analysis lab extensions - `POST /api/dev/analysis/solve/frame` diff --git a/docs/development/ogscope-service-hardening.md b/docs/development/ogscope-service-hardening.md index 8f4b977..3b55c7b 100644 --- a/docs/development/ogscope-service-hardening.md +++ b/docs/development/ogscope-service-hardening.md @@ -33,4 +33,12 @@ sudo dmesg -T | grep -i -E 'oom|killed process' | `OGSCOPE_PREVIEW_JPEG_QUALITY` | 共享预览 JPEG 质量(与调试 MJPEG 默认质量一致)/ Shared preview JPEG quality | | `OGSCOPE_SHARED_PREVIEW_FPS` | 共享抓帧与 MJPEG 推送目标帧率 / Shared grabber and MJPEG pacing FPS | | `OGSCOPE_DEBUG_PREVIEW_MIN_INTERVAL_MS` | 调试「单帧预览」接口每客户端最小间隔(毫秒);过短返回 304 / Min interval for `/api/dev/debug/camera/preview` per client | +| `OGSCOPE_CAMERA_CAPTURE_TIMEOUT_SEC` | 单次 Picamera2 抓帧的硬超时,必须高于最长曝光;超时后复用未完成任务,不重复排队新请求 / Hard timeout for one Picamera2 capture; keep above maximum exposure and do not queue duplicate jobs after timeout | | `OGSCOPE_KEEP_RAW_CACHE` | `1` 时在共享管理器中常驻 `_latest_raw`;默认 `0` 以省内存 / Retain raw frame cache when `1` | + +当抓帧线程、`stop` 或 `close` 无法在限时内结束时,CameraManager 会保留原相机句柄并报告 +`restart_required=true`,禁止同一进程创建第二个 Picamera2 实例。此状态需要重启 `ogscope.service` +来释放 libcamera 资源。/ When a capture worker, `stop`, or `close` cannot finish within +its deadline, CameraManager retains the original handle, reports `restart_required=true`, and +blocks a second Picamera2 instance in the same process. Restart `ogscope.service` to release the +libcamera resources. diff --git a/ogscope/config.py b/ogscope/config.py index 9285ddc..0ab681f 100644 --- a/ogscope/config.py +++ b/ogscope/config.py @@ -375,6 +375,15 @@ class Settings(BaseSettings): le=30.0, description="相机探测超时(秒)/ Camera probe timeout in seconds", ) + camera_capture_timeout_sec: float = Field( + default=4.0, + ge=0.5, + le=120.0, + description=( + "单次相机抓帧硬超时(秒);应高于允许的最长曝光 / " + "Hard timeout for one camera frame; keep above the longest allowed exposure" + ), + ) camera_grab_failures_offline: int = Field( default=3, ge=1, diff --git a/ogscope/config_catalog.py b/ogscope/config_catalog.py index 5f51249..f95cb6b 100644 --- a/ogscope/config_catalog.py +++ b/ogscope/config_catalog.py @@ -94,6 +94,7 @@ "preview_encoder", "debug_preview_min_interval_ms", "camera_probe_timeout_sec", + "camera_capture_timeout_sec", "camera_grab_failures_offline", "camera_idle_shutdown_sec", "camera_frame_stale_timeout_sec", diff --git a/ogscope/domain/camera/focus.py b/ogscope/domain/camera/focus.py new file mode 100644 index 0000000..c4d4253 --- /dev/null +++ b/ogscope/domain/camera/focus.py @@ -0,0 +1,262 @@ +"""星点焦点质量测量 / Star focus quality measurement.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any + +import numpy as np + +from ogscope.algorithms.star_extract import StarExtractor + + +@dataclass(slots=True) +class FocusStarMetric: + """单颗星点的焦点指标 / Focus metrics for one detected star.""" + + x: float + y: float + hfd_px: float + fwhm_px: float + snr: float + roundness: float + concentration: float + peak: float + flux: float + saturated: bool + + def to_dict(self) -> dict[str, Any]: + """返回 JSON 友好的数值 / Return JSON-friendly values.""" + payload = asdict(self) + for key, value in payload.items(): + if isinstance(value, float): + payload[key] = round(value, 4) + return payload + + +class FocusMetricAnalyzer: + """在原始相机帧上测量 HFD 与辅助质量指标 / Measure HFD on raw frames.""" + + def __init__( + self, + *, + max_stars: int = 40, + stamp_radius: int = 24, + min_snr: float = 8.0, + min_roundness: float = 0.35, + ) -> None: + self._extractor = StarExtractor(max_stars=max_stars) + self._stamp_radius = max(8, int(stamp_radius)) + self._min_snr = float(min_snr) + self._min_roundness = float(min_roundness) + + @staticmethod + def _gray(frame: np.ndarray) -> np.ndarray: + """兼容灰度、RGB 与 RGBA 帧 / Accept grayscale, RGB, and RGBA frames.""" + data = np.asarray(frame) + if data.ndim == 2: + return data.astype(np.float32, copy=False) + if data.ndim == 3 and data.shape[2] >= 3: + # 通道均值不依赖 RGB/BGR 顺序 / Channel mean is RGB/BGR agnostic. + return np.mean(data[..., :3], axis=2, dtype=np.float32) + raise ValueError("unsupported focus frame shape / 不支持的焦点分析帧形状") + + def _measure_star( + self, + gray: np.ndarray, + *, + x: float, + y: float, + saturation_level: float, + ) -> FocusStarMetric | None: + radius = self._stamp_radius + cx = int(round(x)) + cy = int(round(y)) + h, w = gray.shape + if cx - radius < 0 or cy - radius < 0: + return None + if cx + radius >= w or cy + radius >= h: + return None + + stamp = gray[cy - radius : cy + radius + 1, cx - radius : cx + radius + 1] + yy, xx = np.indices(stamp.shape, dtype=np.float32) + local_center = float(radius) + initial_r = np.hypot(xx - local_center, yy - local_center) + background_pixels = stamp[initial_r >= radius * 0.78] + if background_pixels.size == 0: + return None + background = float(np.median(background_pixels)) + mad = float(np.median(np.abs(background_pixels - background))) + noise = max(1.0, 1.4826 * mad) + # 阈值化并限制测光孔径,避免暗场正噪声在大窗口内累积后淹没星点轮廓。 + # Threshold and limit the aperture so positive dark-field noise cannot dominate the stellar profile. + signal = np.clip(stamp - background - 1.5 * noise, 0.0, None) + signal[initial_r > radius * 0.65] = 0.0 + total_flux = float(np.sum(signal)) + peak = float(np.max(stamp)) + peak_signal = max(0.0, peak - background) + if total_flux <= 0.0 or peak_signal < max(4.0 * noise, 3.0): + return None + + centroid_x = float(np.sum(signal * xx) / total_flux) + centroid_y = float(np.sum(signal * yy) / total_flux) + dx = xx - centroid_x + dy = yy - centroid_y + radial = np.hypot(dx, dy) + order = np.argsort(radial, axis=None) + sorted_radius = radial.ravel()[order] + cumulative = np.cumsum(signal.ravel()[order]) + half_index = int(np.searchsorted(cumulative, total_flux * 0.5, side="left")) + half_index = min(half_index, sorted_radius.size - 1) + hfd = 2.0 * float(sorted_radius[half_index]) + + var_x = float(np.sum(signal * dx * dx) / total_flux) + var_y = float(np.sum(signal * dy * dy) / total_flux) + sigma_x = float(np.sqrt(max(0.0, var_x))) + sigma_y = float(np.sqrt(max(0.0, var_y))) + sigma_major = max(sigma_x, sigma_y) + sigma_minor = min(sigma_x, sigma_y) + roundness = sigma_minor / sigma_major if sigma_major > 1e-6 else 1.0 + fwhm = 2.35482 * float(np.sqrt(max(0.0, (var_x + var_y) / 2.0))) + + core_flux = float(np.sum(signal[radial <= 1.5])) + reference_flux = float(np.sum(signal[radial <= 4.5])) + concentration = core_flux / reference_flux if reference_flux > 0.0 else 0.0 + aperture_pixels = max(1, int(np.count_nonzero(radial <= radius * 0.72))) + snr = total_flux / float( + np.sqrt(max(total_flux, 0.0) + aperture_pixels * noise**2) + ) + saturated = peak >= saturation_level * 0.98 + + return FocusStarMetric( + x=x, + y=y, + hfd_px=hfd, + fwhm_px=fwhm, + snr=snr, + roundness=roundness, + concentration=concentration, + peak=peak, + flux=total_flux, + saturated=saturated, + ) + + def analyze( + self, + frame: np.ndarray, + *, + frame_id: int, + timestamp: float, + target_x: float | None = None, + target_y: float | None = None, + ) -> dict[str, Any]: + """分析一帧并返回多星稳健统计 / Analyze one frame with robust multi-star statistics.""" + raw = np.asarray(frame) + gray = self._gray(raw) + h, w = gray.shape + if np.issubdtype(raw.dtype, np.integer): + saturation_level = float(np.iinfo(raw.dtype).max) + else: + # 浮点帧通常仍使用 0..1 或 0..255 标度;不能把当前帧峰值当作饱和上限。 + # Float frames commonly use 0..1 or 0..255; the current peak is not a saturation ceiling. + frame_peak = float(np.max(gray)) + saturation_level = 1.0 if frame_peak <= 1.5 else 255.0 + + candidates = self._extractor.extract(raw) + measured: list[FocusStarMetric] = [] + for candidate in candidates: + metric = self._measure_star( + gray, + x=float(candidate.x), + y=float(candidate.y), + saturation_level=saturation_level, + ) + if metric is not None: + measured.append(metric) + + usable = [ + star + for star in measured + if not star.saturated + and star.snr >= self._min_snr + and star.roundness >= self._min_roundness + and 0.2 <= star.hfd_px <= self._stamp_radius * 1.8 + ] + usable.sort(key=lambda star: star.snr, reverse=True) + usable = usable[:20] + + warnings: list[str] = [] + if not candidates: + warnings.append("no_stars_detected") + if any(star.saturated for star in measured): + warnings.append("saturated_stars_rejected") + if candidates and not usable: + warnings.append("no_usable_stars") + elif len(usable) < 3: + warnings.append("low_star_count") + + selected: FocusStarMetric | None = None + if usable and target_x is not None and target_y is not None: + target_px_x = min(1.0, max(0.0, float(target_x))) * max(0, w - 1) + target_px_y = min(1.0, max(0.0, float(target_y))) * max(0, h - 1) + nearest = min( + usable, + key=lambda star: (star.x - target_px_x) ** 2 + + (star.y - target_px_y) ** 2, + ) + max_distance = max(32.0, min(h, w) * 0.12) + distance = float(np.hypot(nearest.x - target_px_x, nearest.y - target_px_y)) + if distance <= max_distance: + selected = nearest + else: + warnings.append("target_star_not_found") + elif usable: + selected = usable[0] + + state = "measuring" + if not usable: + state = "no_stars" + elif len(usable) < 3: + state = "low_confidence" + + aggregate: dict[str, float] | None = None + if usable: + hfd_values = np.asarray([star.hfd_px for star in usable], dtype=np.float64) + fwhm_values = np.asarray( + [star.fwhm_px for star in usable], dtype=np.float64 + ) + concentration_values = np.asarray( + [star.concentration for star in usable], dtype=np.float64 + ) + median_hfd = float(np.median(hfd_values)) + aggregate = { + "median_hfd_px": round(median_hfd, 4), + "hfd_mad_px": round( + float(np.median(np.abs(hfd_values - median_hfd))), 4 + ), + "median_fwhm_px": round(float(np.median(fwhm_values)), 4), + "median_concentration": round( + float(np.median(concentration_values)), 4 + ), + } + + return { + "success": True, + "state": state, + "frame_id": int(frame_id), + "timestamp": float(timestamp), + "frame": {"width": int(w), "height": int(h)}, + "stars_detected": len(candidates), + "stars_measured": len(measured), + "stars_used": len(usable), + "aggregate": aggregate, + "selected_star": selected.to_dict() if selected is not None else None, + "stars": [star.to_dict() for star in usable], + "warnings": warnings, + } + + +focus_metric_analyzer = FocusMetricAnalyzer() + + +__all__ = ["FocusMetricAnalyzer", "FocusStarMetric", "focus_metric_analyzer"] diff --git a/ogscope/domain/camera/services.py b/ogscope/domain/camera/services.py index 5331904..670b187 100644 --- a/ogscope/domain/camera/services.py +++ b/ogscope/domain/camera/services.py @@ -4,6 +4,7 @@ from __future__ import annotations +import asyncio import logging import time from typing import Any @@ -12,6 +13,7 @@ from starlette.requests import Request from ogscope.config import get_settings +from ogscope.domain.camera.focus import focus_metric_analyzer from ogscope.domain.camera.stream_limiter import get_mjpeg_stream_limiter from ogscope.platform.adapters.debug_services import get_debug_services_module @@ -85,6 +87,25 @@ async def get_rate_limited_preview( _PREVIEW_CLIENT_LAST_TS[client_host] = now return await self.get_preview(since_frame_id=since_frame_id) + async def get_focus_metrics( + self, + *, + target_x: float | None = None, + target_y: float | None = None, + ) -> dict[str, Any]: + """读取原始帧并测量星点焦点质量 / Measure star focus quality on a raw frame.""" + from ogscope.web.camera_shared import get_camera_manager + + frame, frame_id, timestamp = await get_camera_manager().get_raw_frame() + return await asyncio.to_thread( + focus_metric_analyzer.analyze, + frame, + frame_id=frame_id, + timestamp=timestamp, + target_x=target_x, + target_y=target_y, + ) + async def get_product_camera_status( self, *, diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 076889d..1954604 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -6,6 +6,7 @@ import logging from abc import ABC, abstractmethod +from concurrent.futures import TimeoutError as FutureTimeoutError from typing import Any, Optional import numpy as np @@ -78,6 +79,7 @@ def __init__(self, config: dict[str, Any]): self._frame_duration_limits: tuple[int, int] | None = None self._lores_available = False self._last_lores_stats: dict[str, Any] = {} + self._pending_capture_job = None # 相机参数 / Camera parameters requested_width = int(config.get("width", 640)) @@ -101,6 +103,9 @@ def __init__(self, config: dict[str, Any]): self.white_balance_gain_b = config.get("white_balance_gain_b", 1.0) self.night_mode = bool(config.get("night_mode", False)) self.auto_exposure_max_us = int(config.get("auto_exposure_max_us", 2_000_000)) + self.capture_timeout_sec = max( + 0.5, float(config.get("capture_timeout_sec", 4.0)) + ) self.ae_flicker_mode = str(config.get("ae_flicker_mode", "off")).lower() self.noise_reduction_mode = self._normalize_noise_reduction_mode( config.get("noise_reduction_mode", config.get("noise_reduction", "fast")) @@ -727,6 +732,7 @@ def stop_capture(self) -> bool: try: self.camera.stop() self.is_capturing = False + self._pending_capture_job = None logger.info("相机停止捕获") return True except Exception as e: @@ -744,9 +750,22 @@ def capture_image(self) -> Optional[np.ndarray]: return None try: - # 同一请求读取图像与元数据,避免额外等待下一帧 - # Read image and metadata from one request to avoid waiting for another frame. - request = self.camera.capture_request() + # 使用 Picamera2 异步任务并限制等待时间,避免 libcamera 卡住后永久占用抓帧线程。 + # Use a bounded Picamera2 job so a stuck libcamera request cannot hold the grabber forever. + if self._pending_capture_job is None: + self._pending_capture_job = self.camera.capture_request(wait=False) + job = self._pending_capture_job + try: + request = self.camera.wait(job, timeout=self.capture_timeout_sec) + except FutureTimeoutError: + logger.error( + "相机抓帧超时(%.2fs),等待生命周期管理器回收 / " + "Camera capture timed out after %.2fs; lifecycle recovery required", + self.capture_timeout_sec, + self.capture_timeout_sec, + ) + return None + self._pending_capture_job = None try: image = request.make_array("main") self._last_metadata = dict(request.get_metadata() or {}) @@ -802,6 +821,7 @@ def capture_image(self) -> Optional[np.ndarray]: return image except Exception as e: + self._pending_capture_job = None logger.error(f"捕获图像失败: {e}") return None diff --git a/ogscope/web/api/debug/routes.py b/ogscope/web/api/debug/routes.py index 320b480..5b37377 100644 --- a/ogscope/web/api/debug/routes.py +++ b/ogscope/web/api/debug/routes.py @@ -175,6 +175,28 @@ async def get_debug_camera_preview( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/debug/camera/focus/metrics") +async def get_debug_camera_focus_metrics( + target_x: float | None = Query(default=None, ge=0.0, le=1.0), + target_y: float | None = Query(default=None, ge=0.0, le=1.0), +): + """测量星点焦点质量,坐标为归一化画幅位置 / Measure focus with normalized target coordinates.""" + if (target_x is None) != (target_y is None): + raise HTTPException( + status_code=422, + detail="target_x and target_y must be provided together / 目标坐标必须成对提供", + ) + try: + return await camera_domain_service.get_focus_metrics( + target_x=target_x, + target_y=target_y, + ) + except RuntimeError as e: + raise HTTPException(status_code=503, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + @router.post("/debug/camera/capture") async def capture_debug_image(): """拍摄单张图片 / Take a single picture""" diff --git a/ogscope/web/api/debug/services.py b/ogscope/web/api/debug/services.py index a01dc6a..42c5045 100644 --- a/ogscope/web/api/debug/services.py +++ b/ogscope/web/api/debug/services.py @@ -915,7 +915,12 @@ async def set_auto_exposure_mode(enabled: bool): if not hasattr(camera, "set_auto_exposure"): raise Exception("当前相机不支持自动曝光切换") - if not camera.set_auto_exposure(bool(enabled)): + applied = await get_camera_manager().reconfigure_camera( + "set_auto_exposure", + lambda: camera.set_auto_exposure(bool(enabled)), + timeout_sec=10.0, + ) + if not applied: raise Exception("设置自动曝光模式失败") get_camera_manager().update_runtime_overrides({"auto_exposure": bool(enabled)}) @@ -933,70 +938,77 @@ async def update_settings(settings: dict[str, Any]): raise Exception("相机未初始化") try: - # 优先处理自动曝光开关,避免自动 / Prioritize the automatic exposure switch to avoid automatic - auto_exposure = settings.get( - "autoExposure", getattr(camera, "auto_exposure", False) - ) - if hasattr(camera, "set_auto_exposure"): - camera.set_auto_exposure(bool(auto_exposure)) - - # 更新基础相机参数 / Update basic camera parameters - if not auto_exposure and "exposure" in settings: - camera.set_exposure(settings["exposure"]) - - if not auto_exposure and "gain" in settings and "digitalGain" in settings: - camera.set_gain(settings["gain"], settings.get("digitalGain", 1.0)) - elif not auto_exposure and "gain" in settings: - camera.set_gain(settings["gain"]) - - # 更新图像增强参数 / Update image enhancement parameters - if any( - key in settings - for key in ["contrast", "brightness", "saturation", "sharpness"] - ): - contrast = settings.get("contrast", 1.0) - brightness = settings.get("brightness", 0.0) - saturation = settings.get("saturation", 1.0) - sharpness = settings.get("sharpness", 1.0) + # 整组设置在同一个相机事务中执行,避免与 capture_request 并发访问 libcamera。 + # Apply the full settings batch in one camera transaction to avoid racing capture_request. + def _apply_settings_sync() -> bool: + auto_exposure = settings.get( + "autoExposure", getattr(camera, "auto_exposure", False) + ) + if hasattr(camera, "set_auto_exposure"): + camera.set_auto_exposure(bool(auto_exposure)) - if hasattr(camera, "set_image_enhancement"): + if not auto_exposure and "exposure" in settings: + camera.set_exposure(settings["exposure"]) + + if ( + not auto_exposure + and "gain" in settings + and "digitalGain" in settings + ): + camera.set_gain(settings["gain"], settings.get("digitalGain", 1.0)) + elif not auto_exposure and "gain" in settings: + camera.set_gain(settings["gain"]) + + if any( + key in settings + for key in ["contrast", "brightness", "saturation", "sharpness"] + ) and hasattr(camera, "set_image_enhancement"): camera.set_image_enhancement( - contrast, brightness, saturation, sharpness + settings.get("contrast", 1.0), + settings.get("brightness", 0.0), + settings.get("saturation", 1.0), + settings.get("sharpness", 1.0), ) - # 更新降噪设置 / Update noise reduction settings - if "noiseReductionMode" in settings: - if hasattr(camera, "set_noise_reduction_mode"): + if "noiseReductionMode" in settings and hasattr( + camera, "set_noise_reduction_mode" + ): camera.set_noise_reduction_mode(settings["noiseReductionMode"]) - if "noiseReduction" in settings and "noiseReductionMode" not in settings: - if hasattr(camera, "set_noise_reduction"): + elif "noiseReduction" in settings and hasattr( + camera, "set_noise_reduction" + ): camera.set_noise_reduction(settings["noiseReduction"]) - # 更新 libcamera 高级控制 / Update advanced libcamera controls - if "aeFlickerMode" in settings: - if hasattr(camera, "set_ae_flicker_mode"): + if "aeFlickerMode" in settings and hasattr( + camera, "set_ae_flicker_mode" + ): camera.set_ae_flicker_mode(settings["aeFlickerMode"]) - if "autoExposureMaxUs" in settings and settings["autoExposureMaxUs"]: - if hasattr(camera, "set_auto_exposure_max_us"): + if ( + "autoExposureMaxUs" in settings + and settings["autoExposureMaxUs"] + and hasattr(camera, "set_auto_exposure_max_us") + ): camera.set_auto_exposure_max_us(settings["autoExposureMaxUs"]) - # 更新白平衡设置 / Update white balance settings - if "whiteBalanceMode" in settings: - mode = settings["whiteBalanceMode"] - gain_r = settings.get("whiteBalanceGainR", 1.0) - gain_b = settings.get("whiteBalanceGainB", 1.0) + if "whiteBalanceMode" in settings and hasattr( + camera, "set_white_balance" + ): + camera.set_white_balance( + settings["whiteBalanceMode"], + settings.get("whiteBalanceGainR", 1.0), + settings.get("whiteBalanceGainB", 1.0), + ) - if hasattr(camera, "set_white_balance"): - camera.set_white_balance(mode, gain_r, gain_b) + if "colorMode" in settings and hasattr(camera, "set_color_mode"): + camera.set_color_mode(settings["colorMode"]) + return True - # 更新颜色模式设置 / Update color mode settings - if "colorMode" in settings: - if hasattr(camera, "set_color_mode"): - await get_camera_manager().reconfigure_camera( - "update_color_mode", - lambda: camera.set_color_mode(settings["colorMode"]), - timeout_sec=10.0, - ) + logging.getLogger(__name__).info( + "camera_settings_apply fields=%s", ",".join(sorted(settings)) + ) + await get_camera_manager().reconfigure_camera( + "update_settings", _apply_settings_sync, timeout_sec=10.0 + ) overrides: dict[str, Any] = {} if "exposure" in settings: diff --git a/ogscope/web/camera_shared.py b/ogscope/web/camera_shared.py index 254d000..6471522 100644 --- a/ogscope/web/camera_shared.py +++ b/ogscope/web/camera_shared.py @@ -43,6 +43,7 @@ def __init__(self) -> None: self._read_lock = Lock() self._frame_lock = Lock() self._grabber_task: asyncio.Task | None = None + self._inflight_read_future: asyncio.Future | None = None self._idle_shutdown_task: asyncio.Task | None = None self._frame_id = 0 self._capture_sequence = 0 @@ -62,12 +63,17 @@ def __init__(self) -> None: self._jpeg_encode_failures = 0 self._target_fps = max(1, int(settings.shared_preview_fps)) self._probe_timeout_sec = max(0.5, float(settings.camera_probe_timeout_sec)) + self._capture_timeout_sec = max(0.5, float(settings.camera_capture_timeout_sec)) + self._grabber_drain_timeout_sec = self._capture_timeout_sec + 1.0 + self._stop_timeout_sec = 4.0 + self._close_timeout_sec = 2.5 self._stale_timeout_sec = max( 0.5, float(settings.camera_frame_stale_timeout_sec) ) self._idle_shutdown_sec = max(0.0, float(settings.camera_idle_shutdown_sec)) self._max_grab_failures = max(1, int(settings.camera_grab_failures_offline)) self._health_error: str | None = None + self._restart_required = False self._consecutive_grab_failures = 0 self._stream_started_at = 0.0 self._last_capture_success_mono = 0.0 @@ -113,6 +119,7 @@ def _build_base_config(self) -> dict[str, Any]: "auto_exposure_max_us": getattr( settings, "camera_auto_exposure_max_us", 2_000_000 ), + "capture_timeout_sec": self._capture_timeout_sec, "ae_flicker_mode": getattr(settings, "camera_ae_flicker_mode", "off"), "noise_reduction_mode": getattr( settings, "camera_noise_reduction_mode", "fast" @@ -194,6 +201,8 @@ def _read_frame_sync(self): def _camera_is_fresh(self) -> bool: """判断运行中的相机是否仍有新鲜帧 / Check whether a running camera is still fresh.""" + if self._restart_required: + return False if self._camera is None or not getattr(self._camera, "is_capturing", False): return False if self._last_capture_success_mono <= 0: @@ -218,18 +227,29 @@ def _cancel_idle_shutdown(self) -> None: async def ensure_started(self, *, start_grabber: bool = False) -> None: """确保单相机进入采集并启动共享帧抓取 / Ensure capture and shared frame grabber.""" self._cancel_idle_shutdown() + if self._restart_required: + raise RuntimeError( + self._health_error + or "相机需要重启服务后恢复 / Camera service restart required" + ) if self._camera_is_fresh(): if start_grabber: async with self._control_lock: await self._ensure_grabber_locked() return async with self._control_lock: + if self._restart_required: + raise RuntimeError( + self._health_error + or "相机需要重启服务后恢复 / Camera service restart required" + ) if self._camera_is_fresh(): if start_grabber: await self._ensure_grabber_locked() return if self._camera is None: self._health_error = None + self._restart_required = False self._has_successful_capture = False self._camera = await asyncio.to_thread(self._create_camera_sync) if self._camera is None: @@ -325,61 +345,98 @@ async def stop(self) -> None: ) return try: - await self._stop_grabber_locked() + if not await self._stop_grabber_locked(): + return if self._camera is None: + self._restart_required = False return - try: - await asyncio.wait_for( - asyncio.to_thread(self._safe_stop_capture_sync), timeout=4.0 - ) - except asyncio.TimeoutError: - self._logger.warning( - "相机停止超时,继续执行退出流程 / Camera stop timed out, continue shutdown" - ) - try: - await asyncio.wait_for( - asyncio.to_thread(self._safe_close_camera_sync), timeout=2.5 - ) - except asyncio.TimeoutError: - self._logger.warning( - "相机关闭超时,继续执行退出流程 / Camera close timed out, continue shutdown" + _, stop_timed_out = await self._run_lifecycle_step_locked( + "stop", self._safe_stop_capture_sync, self._stop_timeout_sec + ) + if stop_timed_out: + return + close_ok, close_timed_out = await self._run_lifecycle_step_locked( + "close", self._safe_close_camera_sync, self._close_timeout_sec + ) + if close_timed_out or not close_ok: + self._mark_restart_required( + "相机资源未能安全关闭,需要重启服务 / " + "Camera resources did not close safely; service restart required" ) - self._camera = None - self._last_capture_success_mono = 0.0 - self._has_successful_capture = False - with self._frame_lock: - self._latest_raw = None - self._latest_jpeg = None - self._latest_ts = 0.0 - self._latest_w = 0 - self._latest_h = 0 + return + self._clear_camera_state_locked(health_error=None) finally: if acquired: self._control_lock.release() - def _safe_stop_capture_sync(self) -> None: + def _safe_stop_capture_sync(self) -> bool: camera = self._camera if camera is None: - return + return True if not getattr(camera, "is_capturing", False): - return + return True try: - camera.stop_capture() + return bool(camera.stop_capture()) except Exception as e: self._logger.warning( "停止相机捕获异常 / Failed to stop camera capture: %s", e ) + return False - def _safe_close_camera_sync(self) -> None: + def _safe_close_camera_sync(self) -> bool: camera = self._camera if camera is None: - return + return True try: inner_camera = getattr(camera, "camera", None) if inner_camera is not None and hasattr(inner_camera, "close"): inner_camera.close() + return True except Exception as e: self._logger.warning("关闭相机资源异常 / Failed to close camera: %s", e) + return False + + def _mark_restart_required(self, reason: str) -> None: + """标记不可在进程内恢复的相机故障 / Mark a camera fault unsafe for in-process recovery.""" + self._restart_required = True + self._health_error = reason + self._logger.critical("camera_restart_required reason=%s", reason) + + def _clear_camera_state_locked(self, *, health_error: str | None) -> None: + """清除已确认释放的相机状态 / Clear camera state only after confirmed release.""" + self._camera = None + self._restart_required = False + self._health_error = health_error + self._consecutive_grab_failures = 0 + self._stream_started_at = 0.0 + self._last_capture_success_mono = 0.0 + self._has_successful_capture = False + with self._frame_lock: + self._latest_raw = None + self._latest_jpeg = None + self._latest_ts = 0.0 + self._latest_w = 0 + self._latest_h = 0 + + async def _run_lifecycle_step_locked( + self, + operation_name: str, + fn: Callable[[], bool], + timeout_sec: float, + ) -> tuple[bool, bool]: + """在读锁内限时执行生命周期步骤 / Run a bounded lifecycle step under the read lock.""" + try: + result = await asyncio.wait_for( + asyncio.to_thread(self._call_with_read_lock, fn), + timeout=max(0.05, float(timeout_sec)), + ) + return bool(result), False + except asyncio.TimeoutError: + self._mark_restart_required( + f"相机{operation_name}超时,需要重启服务 / " + f"Camera {operation_name} timed out; service restart required" + ) + return False, True async def pause_grabber(self) -> None: """暂停共享抓帧任务(保留采集)/ Pause shared frame grabber only.""" @@ -409,16 +466,28 @@ async def reconfigure_camera( """受控重配置:同一临界区内停抓帧->改参->恢复 / Controlled reconfigure.""" async with self._control_lock: t0 = time.time() - await self._stop_grabber_locked() + if not await self._stop_grabber_locked(): + raise RuntimeError( + self._health_error + or "相机抓帧任务未能停止 / Camera grabber did not stop" + ) try: result = await asyncio.wait_for( asyncio.to_thread(self._call_with_read_lock, fn), timeout=timeout_sec, ) return result + except asyncio.TimeoutError: + self._mark_restart_required( + f"相机重配置超时({operation_name}),需要重启服务 / " + f"Camera reconfiguration timed out ({operation_name}); service restart required" + ) + raise finally: - if self._camera is not None and getattr( - self._camera, "is_capturing", False + if ( + not self._restart_required + and self._camera is not None + and getattr(self._camera, "is_capturing", False) ): await self._ensure_grabber_locked() self._logger.info( @@ -442,41 +511,68 @@ async def _invalidate_camera_locked(self, reason: str) -> None: self._health_error = reason current = asyncio.current_task() if self._grabber_task and self._grabber_task is not current: - await self._stop_grabber_locked() + if not await self._stop_grabber_locked(): + return elif self._grabber_task is current: self._grabber_task = None - await asyncio.to_thread(self._safe_stop_capture_sync) - await asyncio.to_thread(self._safe_close_camera_sync) - self._camera = None - self._consecutive_grab_failures = 0 - self._stream_started_at = 0.0 - self._last_capture_success_mono = 0.0 - self._has_successful_capture = False - with self._frame_lock: - self._latest_raw = None - self._latest_jpeg = None - self._latest_ts = 0.0 - self._latest_w = 0 - self._latest_h = 0 + _, stop_timed_out = await self._run_lifecycle_step_locked( + "stop", self._safe_stop_capture_sync, self._stop_timeout_sec + ) + if stop_timed_out: + return + close_ok, close_timed_out = await self._run_lifecycle_step_locked( + "close", self._safe_close_camera_sync, self._close_timeout_sec + ) + if close_timed_out or not close_ok: + self._mark_restart_required( + "相机失效后资源未能安全关闭,需要重启服务 / " + "Camera invalidation could not release resources; service restart required" + ) + return + self._clear_camera_state_locked(health_error=reason) async def _ensure_grabber_locked(self) -> None: if self._grabber_task and not self._grabber_task.done(): return self._grabber_task = asyncio.create_task(self._grabber_loop()) - async def _stop_grabber_locked(self) -> None: + async def _stop_grabber_locked(self) -> bool: if not self._grabber_task: - return - self._grabber_task.cancel() + return True + task = self._grabber_task + if task is asyncio.current_task(): + self._grabber_task = None + return True + task.cancel() try: - await asyncio.wait_for(self._grabber_task, timeout=2.0) + await asyncio.wait_for(task, timeout=2.0) except asyncio.CancelledError: # 抓帧任务被取消属于正常停止流程,不应向上抛出 # Task cancellation is expected during graceful stop. pass except Exception: pass + inflight = self._inflight_read_future + if inflight is not None and not inflight.done(): + try: + # shield 防止 wait_for 取消仍在工作线程中收尾的读请求。 + # shield keeps wait_for from cancelling the read that is draining in a worker thread. + await asyncio.wait_for( + asyncio.shield(inflight), timeout=self._grabber_drain_timeout_sec + ) + except asyncio.TimeoutError: + self._mark_restart_required( + "相机抓帧线程未能在超时内退出,需要重启服务 / " + "Camera read worker did not drain; service restart required" + ) + self._grabber_task = None + return False + except Exception: + pass + if self._inflight_read_future is inflight: + self._inflight_read_future = None self._grabber_task = None + return True async def _grabber_loop(self) -> None: loop = asyncio.get_running_loop() @@ -485,7 +581,16 @@ async def _grabber_loop(self) -> None: interval = 1.0 / float(max(1, self._target_fps)) t0 = time.time() try: - frame = await asyncio.to_thread(self._read_frame_sync) + read_future = loop.run_in_executor(None, self._read_frame_sync) + self._inflight_read_future = read_future + try: + frame = await asyncio.shield(read_future) + finally: + if ( + read_future.done() + and self._inflight_read_future is read_future + ): + self._inflight_read_future = None if frame is not None: self._consecutive_grab_failures = 0 encode_t0 = time.perf_counter() @@ -550,6 +655,8 @@ def ensure_camera_instance_sync(self): def attach_camera_instance(self, camera: Any) -> None: """注入现有相机实例(测试/兼容)/ Attach existing camera instance (tests/compat).""" self._camera = camera + self._restart_required = False + self._health_error = None async def status(self) -> dict[str, Any]: cam = self._camera @@ -558,6 +665,7 @@ async def status(self) -> dict[str, Any]: "connected": False, "streaming": False, "error": self._health_error or "相机未初始化 / Camera not initialized", + "restart_required": bool(self._restart_required), "runtime_overrides": self._runtime_overrides, } initialized = bool(getattr(cam, "is_initialized", False)) @@ -571,7 +679,8 @@ async def status(self) -> dict[str, Any]: and (time.time() - self._stream_started_at) <= self._probe_timeout_sec ) offline = ( - bool(self._health_error) + self._restart_required + or bool(self._health_error) or self._consecutive_grab_failures >= self._max_grab_failures or (capturing and not has_successful_capture and not within_grace) ) @@ -586,12 +695,14 @@ async def status(self) -> dict[str, Any]: "connected": False, "streaming": False, "error": reason, + "restart_required": bool(self._restart_required), "runtime_overrides": self._runtime_overrides, } info = await asyncio.to_thread(cam.get_camera_info) return { "connected": initialized and capturing, "streaming": capturing and (has_successful_capture or within_grace), + "restart_required": False, "info": info, "runtime_overrides": self._runtime_overrides, } diff --git a/tests/unit/test_camera_flip.py b/tests/unit/test_camera_flip.py index 43f714a..150c41b 100644 --- a/tests/unit/test_camera_flip.py +++ b/tests/unit/test_camera_flip.py @@ -2,6 +2,7 @@ import sys import types +from concurrent.futures import TimeoutError as FutureTimeoutError import numpy as np import pytest @@ -73,6 +74,45 @@ def set_controls(self, controls: dict) -> None: self.controls_log.append(dict(controls)) +class _FakeCompletedRequest: + """最小完成请求替身 / Minimal completed-request test double.""" + + def __init__(self) -> None: + self.released = False + + def make_array(self, _stream: str) -> np.ndarray: + return np.zeros((720, 1280, 3), dtype=np.uint8) + + def get_metadata(self) -> dict: + return {"ExposureTime": 10000} + + def release(self) -> None: + self.released = True + + +class _AsyncCapturePicamera: + """记录异步抓帧和有限等待 / Track asynchronous capture and bounded waits.""" + + def __init__(self, *, time_out: bool = False) -> None: + self.time_out = time_out + self.capture_calls = 0 + self.wait_timeouts: list[float] = [] + self.job = object() + self.request = _FakeCompletedRequest() + + def capture_request(self, *, wait: bool): + assert wait is False + self.capture_calls += 1 + return self.job + + def wait(self, job, timeout: float): + assert job is self.job + self.wait_timeouts.append(timeout) + if self.time_out: + raise FutureTimeoutError + return self.request + + @pytest.mark.unit def test_initialize_auto_white_balance_really_enables_awb(monkeypatch) -> None: fake = _FakePicamera2() @@ -112,6 +152,41 @@ def test_initialize_manual_white_balance_sets_colour_gains(monkeypatch) -> None: ) +@pytest.mark.unit +def test_capture_uses_picamera_job_with_bounded_wait() -> None: + fake = _AsyncCapturePicamera() + cam = IMX327MIPICamera(_minimal_config(capture_timeout_sec=3.5)) + cam.camera = fake + cam.is_initialized = True + cam.is_capturing = True + + frame = cam.capture_image() + + assert frame is not None + assert fake.capture_calls == 1 + assert fake.wait_timeouts == [3.5] + assert fake.request.released is True + assert cam._pending_capture_job is None + + +@pytest.mark.unit +def test_capture_timeout_reuses_pending_job_instead_of_queueing_another() -> None: + fake = _AsyncCapturePicamera(time_out=True) + cam = IMX327MIPICamera(_minimal_config(capture_timeout_sec=0.5)) + cam.camera = fake + cam.is_initialized = True + cam.is_capturing = True + + assert cam.capture_image() is None + assert fake.capture_calls == 1 + assert cam._pending_capture_job is fake.job + + fake.time_out = False + assert cam.capture_image() is not None + assert fake.capture_calls == 1 + assert cam._pending_capture_job is None + + @pytest.mark.unit def test_encode_frame_preserves_rgb_channel_order() -> None: cv2 = pytest.importorskip("cv2") diff --git a/tests/unit/test_camera_manager_health.py b/tests/unit/test_camera_manager_health.py index f33d688..098e5ae 100644 --- a/tests/unit/test_camera_manager_health.py +++ b/tests/unit/test_camera_manager_health.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import time import numpy as np import pytest @@ -38,6 +39,28 @@ def get_video_frame(self): return np.zeros((360, 640, 3), dtype=np.uint8) +class _CloseProbe: + def __init__(self) -> None: + self.closed = False + + def close(self) -> None: + self.closed = True + + +class _SlowStopCamera(_FrameCamera): + """模拟底层 stop 暂时卡住 / Simulate a temporarily stuck low-level stop.""" + + def __init__(self) -> None: + super().__init__() + self.camera = _CloseProbe() + self.is_capturing = True + + def stop_capture(self) -> bool: + time.sleep(0.08) + self.is_capturing = False + return True + + @pytest.mark.asyncio async def test_ensure_started_fails_when_no_frames() -> None: manager = CameraManager() @@ -146,3 +169,51 @@ def test_preview_fps_is_independent_runtime_setting() -> None: manager = CameraManager() assert manager.set_preview_fps(12) == 12 assert manager._target_fps == 12 + + +@pytest.mark.asyncio +async def test_stop_timeout_keeps_camera_handle_and_blocks_reacquire() -> None: + """stop 超时后不得丢弃仍占用 libcamera 的实例 / Keep the handle after stop timeout.""" + manager = CameraManager() + camera = _SlowStopCamera() + manager.attach_camera_instance(camera) + manager._stop_timeout_sec = 0.01 + + await manager.stop() + + assert manager.get_camera_instance() is camera + status = await manager.status() + assert status["connected"] is False + assert status["restart_required"] is True + with pytest.raises(RuntimeError, match="restart required|重启服务"): + await manager.ensure_started() + + # 让首次 stop 工作线程收尾,再确认一次显式 stop 可以安全释放。 + # Let the first stop worker drain, then verify an explicit retry can release safely. + await asyncio.sleep(0.1) + manager._stop_timeout_sec = 0.2 + await manager.stop() + assert manager.get_camera_instance() is None + assert camera.camera.closed is True + + +@pytest.mark.asyncio +async def test_reconfigure_timeout_marks_restart_required_and_stops_grabber() -> None: + """重配置超时后不得恢复抓帧 / Do not resume the grabber after reconfigure timeout.""" + manager = CameraManager() + camera = _FrameCamera() + manager.attach_camera_instance(camera) + camera.is_capturing = True + + def _slow_reconfigure() -> bool: + time.sleep(0.08) + return True + + with pytest.raises(asyncio.TimeoutError): + await manager.reconfigure_camera( + "slow_test", _slow_reconfigure, timeout_sec=0.01 + ) + + assert manager._restart_required is True + assert manager._grabber_task is None + await asyncio.sleep(0.1) diff --git a/tests/unit/test_debug_camera_api.py b/tests/unit/test_debug_camera_api.py index a99f487..59d8414 100644 --- a/tests/unit/test_debug_camera_api.py +++ b/tests/unit/test_debug_camera_api.py @@ -238,6 +238,54 @@ def test_debug_camera_image_quality_success(client, fake_camera_env): assert body["quality"]["noise_level"] == 0.1 +@pytest.mark.unit +def test_debug_camera_focus_metrics_success(client, fake_camera_env, monkeypatch): + from ogscope.domain.camera import services as camera_services + + async def _focus_metrics(*, target_x=None, target_y=None): + return { + "success": True, + "state": "measuring", + "frame_id": 10, + "timestamp": 1.0, + "frame": {"width": 640, "height": 360}, + "stars_detected": 4, + "stars_measured": 4, + "stars_used": 4, + "aggregate": { + "median_hfd_px": 2.4, + "hfd_mad_px": 0.2, + "median_fwhm_px": 2.5, + "median_concentration": 0.7, + }, + "selected_star": None, + "stars": [], + "warnings": [], + } + + monkeypatch.setattr( + camera_services.camera_domain_service, + "get_focus_metrics", + _focus_metrics, + ) + response = client.get( + "/api/dev/debug/camera/focus/metrics", + params={"target_x": 0.5, "target_y": 0.5}, + ) + + assert response.status_code == 200 + assert response.json()["aggregate"]["median_hfd_px"] == 2.4 + + +@pytest.mark.unit +def test_debug_camera_focus_metrics_requires_coordinate_pair(client): + response = client.get( + "/api/dev/debug/camera/focus/metrics", params={"target_x": 0.5} + ) + + assert response.status_code == 422 + + @pytest.mark.unit def test_debug_camera_update_settings_success(client, fake_camera_env): payload = { diff --git a/tests/unit/test_focus_metrics.py b/tests/unit/test_focus_metrics.py new file mode 100644 index 0000000..e115aa3 --- /dev/null +++ b/tests/unit/test_focus_metrics.py @@ -0,0 +1,102 @@ +"""星点焦点测量测试 / Star focus metric tests.""" + +from __future__ import annotations + +import cv2 +import numpy as np +import pytest + +from ogscope.domain.camera.focus import FocusMetricAnalyzer + + +def _star_field( + *, sigma: float, saturated: bool = False, noise_std: float = 0.0 +) -> np.ndarray: + """生成可重复的合成星场 / Build a deterministic synthetic star field.""" + height, width = 240, 320 + yy, xx = np.indices((height, width), dtype=np.float32) + frame = np.full((height, width), 8.0, dtype=np.float32) + stars = [(70, 62, 150.0), (160, 78, 125.0), (245, 135, 180.0), (105, 175, 110.0)] + for x, y, amplitude in stars: + value = 255.0 if saturated and x == 70 else amplitude + frame += value * np.exp(-((xx - x) ** 2 + (yy - y) ** 2) / (2.0 * sigma**2)) + if noise_std > 0: + rng = np.random.default_rng(42) + frame += rng.normal(0.0, noise_std, frame.shape) + frame = np.clip(frame, 0.0, 255.0).astype(np.uint8) + return cv2.cvtColor(frame, cv2.COLOR_GRAY2RGB) + + +@pytest.mark.unit +def test_focus_hfd_increases_with_blur() -> None: + analyzer = FocusMetricAnalyzer(min_snr=4.0) + + sharp = analyzer.analyze(_star_field(sigma=1.4), frame_id=1, timestamp=1.0) + blurred = analyzer.analyze(_star_field(sigma=4.2), frame_id=2, timestamp=2.0) + + assert sharp["state"] == "measuring" + assert blurred["state"] == "measuring" + assert sharp["stars_used"] >= 3 + assert blurred["stars_used"] >= 3 + assert sharp["aggregate"]["median_hfd_px"] < blurred["aggregate"]["median_hfd_px"] + assert ( + sharp["aggregate"]["median_concentration"] + > blurred["aggregate"]["median_concentration"] + ) + + +@pytest.mark.unit +def test_focus_hfd_remains_directional_with_sensor_noise() -> None: + analyzer = FocusMetricAnalyzer(min_snr=4.0) + + sharp = analyzer.analyze( + _star_field(sigma=1.4, noise_std=4.0), frame_id=11, timestamp=11.0 + ) + blurred = analyzer.analyze( + _star_field(sigma=4.2, noise_std=4.0), frame_id=12, timestamp=12.0 + ) + + assert sharp["stars_used"] >= 3 + assert blurred["stars_used"] >= 3 + assert sharp["aggregate"]["median_hfd_px"] < blurred["aggregate"]["median_hfd_px"] + + +@pytest.mark.unit +def test_focus_reports_empty_frame_without_false_score() -> None: + analyzer = FocusMetricAnalyzer() + frame = np.full((120, 160, 3), 10, dtype=np.uint8) + + result = analyzer.analyze(frame, frame_id=3, timestamp=3.0) + + assert result["state"] == "no_stars" + assert result["aggregate"] is None + assert result["selected_star"] is None + assert "no_stars_detected" in result["warnings"] + + +@pytest.mark.unit +def test_focus_selects_nearest_target_star() -> None: + analyzer = FocusMetricAnalyzer(min_snr=4.0) + result = analyzer.analyze( + _star_field(sigma=1.8), + frame_id=4, + timestamp=4.0, + target_x=245 / 320, + target_y=135 / 240, + ) + + selected = result["selected_star"] + assert selected is not None + assert selected["x"] == pytest.approx(245, abs=3) + assert selected["y"] == pytest.approx(135, abs=3) + + +@pytest.mark.unit +def test_focus_rejects_saturated_star_from_aggregate() -> None: + analyzer = FocusMetricAnalyzer(min_snr=4.0) + result = analyzer.analyze( + _star_field(sigma=1.8, saturated=True), frame_id=5, timestamp=5.0 + ) + + assert result["stars_used"] >= 3 + assert "saturated_stars_rejected" in result["warnings"] diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index 62bdfbc..ba9a122 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -1,13 +1,15 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from "react"; import { Camera, Circle, + Crosshair, Download, FileText, FolderOpen, Info, Moon, Play, + RotateCcw, Save, Settings2, SlidersHorizontal, @@ -122,6 +124,42 @@ type StreamMetrics = { cma_free_kb?: number; }; +type FocusStarMetric = { + x: number; + y: number; + hfd_px: number; + fwhm_px: number; + snr: number; + roundness: number; + concentration: number; + saturated: boolean; +}; + +type FocusMetrics = { + success: boolean; + state: "measuring" | "low_confidence" | "no_stars"; + frame_id: number; + timestamp: number; + frame: { width: number; height: number }; + stars_detected: number; + stars_measured: number; + stars_used: number; + aggregate: { + median_hfd_px: number; + hfd_mad_px: number; + median_fwhm_px: number; + median_concentration: number; + } | null; + selected_star: FocusStarMetric | null; + stars: FocusStarMetric[]; + warnings: string[]; +}; + +type FocusSample = { + frameId: number; + hfd: number; +}; + type CameraForm = { exposure: number; gain: number; @@ -207,6 +245,34 @@ function formatSize(bytes: number): string { return `${val.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; } +function FocusHistoryChart({ samples, emptyLabel }: { samples: FocusSample[]; emptyLabel: string }) { + if (samples.length < 2) { + return ( +
+ {emptyLabel} +
+ ); + } + const values = samples.map((sample) => sample.hfd); + const min = Math.min(...values); + const max = Math.max(...values); + const spread = Math.max(0.2, max - min); + const points = values + .map((value, index) => { + const x = (index / Math.max(1, values.length - 1)) * 320; + const y = 76 - ((value - min) / spread) * 60; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(" "); + return ( + + + + + + ); +} + /** * 用 stream/status 探测是否还有 MJPEG 名额(勿 fetch MJPEG URL,否则会额外占用 try_acquire 与长连接重叠)。 * Check limiter via JSON; never fetch the MJPEG URL for probe (that consumes a slot and overlaps with ). @@ -300,6 +366,13 @@ export function CameraConsoleApp() { const [liveFps, setLiveFps] = useState(0); /** 与 OGSCOPE_SHARED_PREVIEW_FPS 一致:共享抓帧与 MJPEG 最小帧间隔 / Env stream pacing cap */ const [streamMetrics, setStreamMetrics] = useState(null); + const [focusActive, setFocusActive] = useState(false); + const [focusBusy, setFocusBusy] = useState(false); + const [focusMetrics, setFocusMetrics] = useState(null); + const [focusHistory, setFocusHistory] = useState([]); + const [focusBestHfd, setFocusBestHfd] = useState(null); + const [focusTarget, setFocusTarget] = useState<{ x: number; y: number } | null>(null); + const [focusError, setFocusError] = useState(null); const [recordElapsed, setRecordElapsed] = useState(0); const [rotationValue, setRotationValue] = useState(180); const [flipHorizontal, setFlipHorizontal] = useState(false); @@ -342,6 +415,7 @@ export function CameraConsoleApp() { const histogramCtxRef = useRef(null); const recordTickRef = useRef(null); const reconnectTimerRef = useRef(null); + const focusRequestInFlightRef = useRef(false); const previewActiveRef = useRef(false); const streamStartedAtRef = useRef(null); const fpsFrameTsRef = useRef([]); @@ -375,8 +449,8 @@ export function CameraConsoleApp() { } }; - const startPreview = async () => { - if (previewBusy) return; + const startPreview = async (): Promise => { + if (previewBusy) return false; setPreviewBusy(true); setErr(null); setPreviewStreamHint(null); @@ -391,12 +465,12 @@ export function CameraConsoleApp() { if (probe === "busy") { setPreviewStreamHint(t("cam.err.streamBusy")); setPreviewStreamIsBusy(true); - return; + return false; } if (probe === "fail") { setPreviewStreamHint(t("cam.err.streamProbeFailed")); setPreviewStreamIsBusy(false); - return; + return false; } setPreviewActive(true); previewActiveRef.current = true; @@ -405,13 +479,76 @@ export function CameraConsoleApp() { streamStartedAtRef.current = performance.now(); setStreamNonce(nonce); await updateCameraStatus(); + return true; } catch (e) { setErr(e instanceof Error ? e.message : String(e)); + return false; } finally { setPreviewBusy(false); } }; + const startFocusCalibration = async () => { + if (focusBusy) return; + setFocusBusy(true); + setFocusError(null); + try { + const ready = previewActive || (await startPreview()); + if (!ready) return; + setFocusMetrics(null); + setFocusHistory([]); + setFocusBestHfd(null); + setFocusTarget(null); + setHistCollapsed(true); + setFocusActive(true); + window.setTimeout(() => { + document.getElementById("focus-calibration-panel")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 0); + } finally { + setFocusBusy(false); + } + }; + + const stopFocusCalibration = () => { + setFocusActive(false); + setFocusError(null); + }; + + const resetFocusBest = () => { + setFocusHistory([]); + setFocusBestHfd(null); + setFocusError(null); + }; + + const handleFocusPreviewClick = (event: ReactMouseEvent) => { + if (!focusActive || !imgRef.current) return; + if ((event.target as HTMLElement).closest("button, label, input")) return; + const image = imgRef.current; + const rect = event.currentTarget.getBoundingClientRect(); + const naturalWidth = image.naturalWidth || focusMetrics?.frame.width || 0; + const naturalHeight = image.naturalHeight || focusMetrics?.frame.height || 0; + if (naturalWidth <= 0 || naturalHeight <= 0) return; + const scale = Math.min(rect.width / naturalWidth, rect.height / naturalHeight); + const shownWidth = naturalWidth * scale; + const shownHeight = naturalHeight * scale; + const offsetX = (rect.width - shownWidth) / 2; + const offsetY = (rect.height - shownHeight) / 2; + const x = (event.clientX - rect.left - offsetX) / shownWidth; + const y = (event.clientY - rect.top - offsetY) / shownHeight; + if (x < 0 || x > 1 || y < 0 || y > 1) return; + setFocusTarget({ x, y }); + setFocusHistory([]); + setFocusBestHfd(null); + setFocusError(null); + }; + + const resumeFocusAutoSelection = () => { + setFocusTarget(null); + setFocusHistory([]); + setFocusBestHfd(null); + setFocusError(null); + }; + const stopPreview = async () => { if (previewBusy) return; setPreviewBusy(true); @@ -940,9 +1077,56 @@ export function CameraConsoleApp() { previewActiveRef.current = previewActive; if (!previewActive) { clearReconnectTimer(); + setFocusActive(false); } }, [previewActive]); + useEffect(() => { + if (!focusActive || !previewActive) return; + let cancelled = false; + const pullFocus = async () => { + if (focusRequestInFlightRef.current || document.hidden) return; + focusRequestInFlightRef.current = true; + try { + const params = new URLSearchParams(); + if (focusTarget) { + params.set("target_x", focusTarget.x.toFixed(6)); + params.set("target_y", focusTarget.y.toFixed(6)); + } + const suffix = params.size > 0 ? `?${params.toString()}` : ""; + const result = await requestJson( + `${debugApi("/camera/focus/metrics")}${suffix}`, + { cache: "no-store" }, + ); + if (cancelled) return; + setFocusMetrics(result); + setFocusError(null); + const hfd = focusTarget + ? result.selected_star?.hfd_px + : result.aggregate?.median_hfd_px; + if (typeof hfd === "number" && Number.isFinite(hfd)) { + setFocusHistory((previous) => { + if (previous.at(-1)?.frameId === result.frame_id) return previous; + return [...previous, { frameId: result.frame_id, hfd }].slice(-60); + }); + setFocusBestHfd((previous) => previous == null ? hfd : Math.min(previous, hfd)); + } + } catch (error) { + if (!cancelled) { + setFocusError(error instanceof Error ? error.message : String(error)); + } + } finally { + focusRequestInFlightRef.current = false; + } + }; + void pullFocus(); + const timer = window.setInterval(pullFocus, 850); + return () => { + cancelled = true; + window.clearInterval(timer); + }; + }, [focusActive, previewActive, focusTarget?.x, focusTarget?.y]); + // MJPEG 在 上通常不按帧触发 onLoad;勿用 lastFrameTime 做断流重连以免与单槽位冲突导致 503 // MJPEG rarely fires onLoad per frame; do not watchdog-reconnect on lastFrameTime (503 with single slot) @@ -1079,6 +1263,41 @@ export function CameraConsoleApp() { const canStopPreview = !previewBusy && previewActive; const canCapture = !previewBusy && !captureBusy && isStreaming; const canRecordToggle = !previewBusy && !recordBusy && isStreaming; + const focusSelectedMetric = focusTarget ? focusMetrics?.selected_star : null; + const focusHfd = focusTarget + ? focusSelectedMetric?.hfd_px ?? null + : focusMetrics?.aggregate?.median_hfd_px ?? null; + const focusFwhm = focusTarget + ? focusSelectedMetric?.fwhm_px ?? null + : focusMetrics?.aggregate?.median_fwhm_px ?? null; + const focusConcentration = focusTarget + ? focusSelectedMetric?.concentration ?? null + : focusMetrics?.aggregate?.median_concentration ?? null; + const focusDeltaPercent = focusHfd != null && focusBestHfd != null && focusBestHfd > 0 + ? ((focusHfd - focusBestHfd) / focusBestHfd) * 100 + : null; + const focusRecent = focusHistory.slice(-3).map((sample) => sample.hfd); + const focusPrevious = focusHistory.slice(-6, -3).map((sample) => sample.hfd); + const focusAverage = (values: number[]) => values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length); + let focusGuidanceKey = "cam.focus.guidance.collecting"; + if (focusMetrics?.state === "no_stars") { + focusGuidanceKey = "cam.focus.guidance.noStars"; + } else if (focusHistory.length >= 5 && focusDeltaPercent != null && focusDeltaPercent <= 3) { + focusGuidanceKey = "cam.focus.guidance.best"; + } else if (focusRecent.length === 3 && focusPrevious.length === 3) { + const trend = focusAverage(focusRecent) - focusAverage(focusPrevious); + const threshold = Math.max(0.04, focusAverage(focusPrevious) * 0.02); + focusGuidanceKey = trend < -threshold + ? "cam.focus.guidance.improving" + : trend > threshold + ? "cam.focus.guidance.worsening" + : "cam.focus.guidance.steady"; + } + const focusConfidenceKey = (focusMetrics?.stars_used ?? 0) >= 5 + ? "cam.focus.confidence.high" + : (focusMetrics?.stars_used ?? 0) >= 3 + ? "cam.focus.confidence.medium" + : "cam.focus.confidence.low"; const totalFilePages = Math.max(1, Math.ceil(files.length / FILE_PAGE_SIZE)); const filePageClamped = Math.min(filePage, totalFilePages); const fileStart = (filePageClamped - 1) * FILE_PAGE_SIZE; @@ -1218,8 +1437,19 @@ export function CameraConsoleApp() { {t("cam.hint.mjpegSingleStream")}

-
- {t("cam.preview.state")}: {previewActive ? t("cam.state.streaming") : t("cam.state.idle")} +
+ + + {t("cam.preview.state")}: {previewActive ? t("cam.state.streaming") : t("cam.state.idle")} +
{previewStreamHint && !previewActive && ( @@ -1239,7 +1469,10 @@ export function CameraConsoleApp() { )} )} -
+
{previewActive && previewStreamHint && (
)} + {previewActive && focusActive && focusMetrics?.frame ? ( + + ) : null} {status?.recording && (
{t("cam.state.rec")} @@ -1336,6 +1606,136 @@ export function CameraConsoleApp() {
)}
+
+
+
+
+ +

{t("cam.focus.title")}

+ + {focusActive ? t("cam.focus.active") : t("cam.focus.idle")} + +
+

+ {t("cam.focus.intro")} +

+
+
+ {!focusActive ? ( + + ) : ( + + )} + + +
+
+ + {focusActive ? ( +
+
+
+
+ {t(focusTarget ? "cam.focus.currentHfd.single" : "cam.focus.currentHfd.multi")} +
+
+ {focusHfd == null ? "—" : focusHfd.toFixed(2)} + px · {t("cam.focus.lowerBetter")} +
+

+ {t(focusGuidanceKey)} +

+
+
+
{t("cam.focus.bestHfd")}
+
{focusBestHfd == null ? "—" : `${focusBestHfd.toFixed(2)} px`}
+
+ {focusDeltaPercent == null ? "—" : `+${Math.max(0, focusDeltaPercent).toFixed(1)}%`} +
+
+
+
{t("cam.focus.stars")}
+
{focusMetrics?.stars_used ?? 0} / {focusMetrics?.stars_detected ?? 0}
+
{t(focusConfidenceKey)}
+
+
+
{t("cam.focus.fwhm")}
+
{focusFwhm == null ? "—" : `${focusFwhm.toFixed(2)} px`}
+
+
+
{t("cam.focus.concentration")}
+
{focusConcentration == null ? "—" : `${(focusConcentration * 100).toFixed(0)}%`}
+
+
+
{t("cam.focus.selectedStar")}
+
{focusMetrics?.selected_star ? `HFD ${focusMetrics.selected_star.hfd_px.toFixed(2)}` : "—"}
+
+ {focusTarget ? t("cam.focus.manualTarget") : t("cam.focus.autoTarget")} +
+
+
+
{t("cam.focus.stability")}
+
{focusMetrics?.aggregate ? `±${focusMetrics.aggregate.hfd_mad_px.toFixed(2)} px` : "—"}
+
+
+
+
+ {t("cam.focus.history")} + {t("cam.focus.clickHint")} +
+ + {focusError ? ( +

{focusError}

+ ) : null} + {focusMetrics?.warnings.includes("saturated_stars_rejected") ? ( +

+ {t("cam.focus.warning.saturated")} +

+ ) : null} + {focusMetrics?.warnings.includes("target_star_not_found") ? ( +

+ {t("cam.focus.warning.targetNotFound")} +

+ ) : null} +
+
+ ) : ( +
+
1. {t("cam.focus.step.start")}
+
2. {t("cam.focus.step.adjust")}
+
3. {t("cam.focus.step.lock")}
+
+ )} +
{t("cam.stats.frameFps")}: diff --git a/web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js b/web/static/analysis-lab/assets/analysis-CpY6-RHN.js similarity index 96% rename from web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js rename to web/static/analysis-lab/assets/analysis-CpY6-RHN.js index 0854fa4..46dd5bc 100644 --- a/web/static/analysis-lab/assets/analysis-Dd7iQ8pr.js +++ b/web/static/analysis-lab/assets/analysis-CpY6-RHN.js @@ -1,51 +1,46 @@ -var As=Object.defineProperty;var Ua=e=>{throw TypeError(e)};var Us=(e,s,n)=>s in e?As(e,s,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[s]=n;var O=(e,s,n)=>Us(e,typeof s!="symbol"?s+"":s,n),Ba=(e,s,n)=>s.has(e)||Ua("Cannot "+n);var h=(e,s,n)=>(Ba(e,s,"read from private field"),n?n.call(e):s.get(e)),te=(e,s,n)=>s.has(e)?Ua("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(e):s.set(e,n),Oe=(e,s,n,r)=>(Ba(e,s,"write to private field"),r?r.call(e,n):s.set(e,n),n);import{j as t,r as i,a as Bs,R as Hs}from"./client-D1ZVDB-N.js";import{c as q,u as Te,T as ss,I as zs}from"./index-CV2LaaT8.js";import{R as Ws}from"./refresh-cw-CIJtimxg.js";/** +var As=Object.defineProperty;var Ua=e=>{throw TypeError(e)};var Us=(e,s,n)=>s in e?As(e,s,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[s]=n;var O=(e,s,n)=>Us(e,typeof s!="symbol"?s+"":s,n),Ba=(e,s,n)=>s.has(e)||Ua("Cannot "+n);var h=(e,s,n)=>(Ba(e,s,"read from private field"),n?n.call(e):s.get(e)),te=(e,s,n)=>s.has(e)?Ua("Cannot add the same private member more than once"):s instanceof WeakSet?s.add(e):s.set(e,n),Oe=(e,s,n,r)=>(Ba(e,s,"write to private field"),r?r.call(e,n):s.set(e,n),n);import{j as t,r as i,a as Bs,R as Hs}from"./client-D1ZVDB-N.js";import{c as Y,u as Te,T as ss,I as zs}from"./index-DfDaIsmY.js";import{R as Ws}from"./refresh-cw-BwAka9-0.js";import{R as Ha}from"./rotate-ccw-B0-EEdqV.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 Fe=q("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + */const Fe=Y("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** * @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 Vs=q("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** + */const Vs=Y("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** * @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 Ha=q("Grid3x3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M3 9h18",key:"1pudct"}],["path",{d:"M3 15h18",key:"5xshup"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** + */const za=Y("Grid3x3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M3 9h18",key:"1pudct"}],["path",{d:"M3 15h18",key:"5xshup"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** * @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 Gs=q("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/** + */const Gs=Y("History",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M12 7v5l4 2",key:"1fdv2h"}]]);/** * @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 Ks=q("House",[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",key:"5wwlr5"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"1d0kgt"}]]);/** + */const Ks=Y("House",[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8",key:"5wwlr5"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",key:"1d0kgt"}]]);/** * @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 st=q("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** + */const st=Y("LoaderCircle",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** * @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 za=q("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);/** + */const qs=Y("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** * @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 qs=q("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** + */const Wa=Y("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]);/** * @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 Wa=q("ZoomIn",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14",key:"1vmskp"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]);/** - * @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 Va=q("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),_="/api/dev";async function W(e){if((e.headers.get("content-type")||"").includes("application/json"))return e.json();const n=await e.text();throw new Error(n||`HTTP ${e.status}`)}async function Js(){const e=await fetch(`${_}/analysis/uploads`);if(!e.ok)throw new Error(await e.text());return e.json()}async function Zs(e,s){const n=new URLSearchParams;s!=null&&s.deleteExperiments&&n.set("delete_experiments","true");const r=n.toString(),o=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}${r?`?${r}`:""}`,{method:"DELETE"});if(!o.ok)throw new Error(await o.text());return await o.json().catch(()=>({}))}async function Bt(e,s="analysis_upload"){const n=new FormData;n.append("file",e),n.append("source",s);const r=await fetch(`${_}/analysis/upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}async function Ys(e){const s=await fetch(`${_}/analysis/uploads/import_from_debug`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:e})}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Ga(e){const s=await fetch(`${_}/analysis/uploads/replace_video`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Xs(e,s){const n=await fetch(`${_}/analysis/solve/image`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,...s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Qs(e,s){const n=await fetch(`${_}/analysis/solve/batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,runs:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Ht(e){const s=await fetch(`${_}/analysis/presets?scope=${e}`);if(!s.ok)throw new Error(await s.text());return s.json()}async function en(e,s){const n=await fetch(`${_}/analysis/presets`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,params:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function zt(e){const s=await fetch(`${_}/analysis/experiments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function tn(e){const s=await fetch(`${_}/analysis/experiments/${encodeURIComponent(e)}`,{method:"DELETE"});if(!s.ok)throw new Error(await s.text())}async function Wt(e,s,n=30){const r=new URLSearchParams({page:String(s),page_size:String(n)});e&&r.set("q",e);const o=await fetch(`${_}/analysis/experiments?${r}`);if(!o.ok)throw new Error(await o.text());return o.json()}async function an(){const e=await fetch(`${_}/debug/files`);if(!e.ok)throw new Error(await e.text());return e.json()}function sn(e){return`${_}/debug/files/${encodeURIComponent(e)}`}async function Ka(e){const s=await fetch(`${_}/debug/files/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function nn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}function Vt(e){return`${_}/analysis/uploads/file?filename=${encodeURIComponent(e)}`}async function qa(e){const s=await fetch(`${_}/analysis/experiments/export?format=${e}`);if(!s.ok)throw new Error(await s.text());return s.text()}async function rn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/experiment_count`);if(!s.ok)throw new Error(await s.text());return s.json()}async function ln(){const e=await fetch(`${_}/analysis/settings`);if(!e.ok)throw new Error(await e.text());return e.json()}async function on(e){const s=await fetch(`${_}/analysis/solve/frame`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function cn(e,s){const n=new FormData;n.append("file",e,"frame.jpg"),n.append("payload",JSON.stringify(s));const r=await fetch(`${_}/analysis/solve/frame_upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}function dn(e){return`${_}/analysis/experiments/${encodeURIComponent(e)}/asset`}async function un(){const e=await fetch("/api/dev/system/info");if(!e.ok)throw new Error(await e.text());return e.json()}function ns(e,s,n,r,o){var g,y,R,d;if(!r)return;if(e.clearRect(0,0,s,n),o.all&&Array.isArray(r.stars_all_centroids)){e.fillStyle="rgba(156, 163, 175, 0.85)";for(const m of r.stars_all_centroids)e.beginPath(),e.arc(m.x,m.y,2.4,0,Math.PI*2),e.fill()}if(o.rejected&&Array.isArray(r.stars_rejected_centroids)){e.strokeStyle="rgba(248, 113, 113, 0.95)",e.lineWidth=1.6;for(const m of r.stars_rejected_centroids)e.beginPath(),e.moveTo(m.x-4,m.y-4),e.lineTo(m.x+4,m.y+4),e.moveTo(m.x+4,m.y-4),e.lineTo(m.x-4,m.y+4),e.stroke()}if(o.pattern&&Array.isArray(r.stars_pattern)){e.strokeStyle="rgba(251, 146, 60, 0.95)",e.lineWidth=2;for(const m of r.stars_pattern)e.beginPath(),e.arc(m.x,m.y,6,0,Math.PI*2),e.stroke()}if(o.matched&&Array.isArray(r.stars_matched)){e.strokeStyle="rgba(34, 197, 94, 0.95)",e.fillStyle="rgba(34, 197, 94, 0.95)",e.lineWidth=2,e.font="11px system-ui, sans-serif";for(const m of r.stars_matched)e.beginPath(),e.arc(m.x,m.y,7,0,Math.PI*2),e.stroke(),m.mag!=null&&e.fillText(`m${Number(m.mag).toFixed(1)}`,m.x+4,m.y-4)}const u=r.overlay_ext;if(Array.isArray(u==null?void 0:u.labels_topn)&&u.labels_topn.length>0){e.fillStyle="rgba(96, 165, 250, 0.95)",e.font="12px system-ui, sans-serif";for(const m of u.labels_topn){if(typeof(m==null?void 0:m.x)!="number"||typeof(m==null?void 0:m.y)!="number")continue;const b=typeof m.mag=="number"?` m${m.mag.toFixed(1)}`:"",x=`${m.name??"Star"}${b}`;e.fillText(x,m.x+8,m.y+14)}}const p=u==null?void 0:u.polar_guide;if(p&&typeof((g=p.frame_center)==null?void 0:g.x)=="number"&&typeof((y=p.frame_center)==null?void 0:y.y)=="number"&&typeof((R=p.target)==null?void 0:R.x)=="number"&&typeof((d=p.target)==null?void 0:d.y)=="number"){const m=p.frame_center.x,b=p.frame_center.y,x=p.target.x,I=p.target.y;e.strokeStyle="rgba(244, 63, 94, 0.95)",e.fillStyle="rgba(244, 63, 94, 0.95)",e.lineWidth=2,e.beginPath(),e.moveTo(m,b),e.lineTo(x,I),e.stroke(),e.beginPath(),e.arc(x,I,6,0,Math.PI*2),e.stroke();const M=typeof p.angular_sep_deg=="number"?p.angular_sep_deg.toFixed(2):"-";e.font="12px system-ui, sans-serif",e.fillText(`Polar ${M}deg`,x+10,I-6)}}function Ja(e,s,n,r){if(!n)return;const o=s.naturalWidth||1,u=s.naturalHeight||1;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}function mn(e,s,n,r){if(!n)return;const o=s.videoWidth||1,u=s.videoHeight||1;if(o<2||u<2)return;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}var N;(function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"})(N||(N={}));const xn=(()=>{let e=0;return()=>e++})(),pn=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),fn=new Error("called FFmpeg.terminate()");var A,ae,Y,ue,me,ot,P;class hn{constructor(){te(this,A,null);te(this,ae,{});te(this,Y,{});te(this,ue,[]);te(this,me,[]);O(this,"loaded",!1);te(this,ot,()=>{h(this,A)&&(h(this,A).onmessage=({data:{id:s,type:n,data:r}})=>{switch(n){case N.LOAD:this.loaded=!0,h(this,ae)[s](r);break;case N.MOUNT:case N.UNMOUNT:case N.EXEC:case N.FFPROBE:case N.WRITE_FILE:case N.READ_FILE:case N.DELETE_FILE:case N.RENAME:case N.CREATE_DIR:case N.LIST_DIR:case N.DELETE_DIR:h(this,ae)[s](r);break;case N.LOG:h(this,ue).forEach(o=>o(r));break;case N.PROGRESS:h(this,me).forEach(o=>o(r));break;case N.ERROR:h(this,Y)[s](r);break}delete h(this,ae)[s],delete h(this,Y)[s]})});te(this,P,({type:s,data:n},r=[],o)=>h(this,A)?new Promise((u,p)=>{const g=xn();h(this,A)&&h(this,A).postMessage({id:g,type:s,data:n},r),h(this,ae)[g]=u,h(this,Y)[g]=p,o==null||o.addEventListener("abort",()=>{p(new DOMException(`Message # ${g} was aborted`,"AbortError"))},{once:!0})}):Promise.reject(pn));O(this,"load",({classWorkerURL:s,...n}={},{signal:r}={})=>(h(this,A)||(Oe(this,A,s?new Worker(new URL(s,import.meta.url),{type:"module"}):new Worker(new URL("/static/analysis-lab/assets/worker-BAOIWoxA.js",import.meta.url),{type:"module"})),h(this,ot).call(this)),h(this,P).call(this,{type:N.LOAD,data:n},void 0,r)));O(this,"exec",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.EXEC,data:{args:s,timeout:n}},void 0,r));O(this,"ffprobe",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.FFPROBE,data:{args:s,timeout:n}},void 0,r));O(this,"terminate",()=>{const s=Object.keys(h(this,Y));for(const n of s)h(this,Y)[n](fn),delete h(this,Y)[n],delete h(this,ae)[n];h(this,A)&&(h(this,A).terminate(),Oe(this,A,null),this.loaded=!1)});O(this,"writeFile",(s,n,{signal:r}={})=>{const o=[];return n instanceof Uint8Array&&o.push(n.buffer),h(this,P).call(this,{type:N.WRITE_FILE,data:{path:s,data:n}},o,r)});O(this,"mount",(s,n,r)=>{const o=[];return h(this,P).call(this,{type:N.MOUNT,data:{fsType:s,options:n,mountPoint:r}},o)});O(this,"unmount",s=>{const n=[];return h(this,P).call(this,{type:N.UNMOUNT,data:{mountPoint:s}},n)});O(this,"readFile",(s,n="binary",{signal:r}={})=>h(this,P).call(this,{type:N.READ_FILE,data:{path:s,encoding:n}},void 0,r));O(this,"deleteFile",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_FILE,data:{path:s}},void 0,n));O(this,"rename",(s,n,{signal:r}={})=>h(this,P).call(this,{type:N.RENAME,data:{oldPath:s,newPath:n}},void 0,r));O(this,"createDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.CREATE_DIR,data:{path:s}},void 0,n));O(this,"listDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.LIST_DIR,data:{path:s}},void 0,n));O(this,"deleteDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_DIR,data:{path:s}},void 0,n))}on(s,n){s==="log"?h(this,ue).push(n):s==="progress"&&h(this,me).push(n)}off(s,n){s==="log"?Oe(this,ue,h(this,ue).filter(r=>r!==n)):s==="progress"&&Oe(this,me,h(this,me).filter(r=>r!==n))}}A=new WeakMap,ae=new WeakMap,Y=new WeakMap,ue=new WeakMap,me=new WeakMap,ot=new WeakMap,P=new WeakMap;var Za;(function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"})(Za||(Za={}));const bn=new Error("failed to get response body reader"),vn=new Error("failed to complete download"),gn="Content-Length",yn=e=>new Promise((s,n)=>{const r=new FileReader;r.onload=()=>{const{result:o}=r;o instanceof ArrayBuffer?s(new Uint8Array(o)):s(new Uint8Array)},r.onerror=o=>{var u,p;n(Error(`File could not be read! Code=${((p=(u=o==null?void 0:o.target)==null?void 0:u.error)==null?void 0:p.code)||-1}`))},r.readAsArrayBuffer(e)}),wn=async e=>{let s;if(typeof e=="string")/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?s=atob(e.split(",")[1]).split("").map(n=>n.charCodeAt(0)):s=await(await fetch(e)).arrayBuffer();else if(e instanceof URL)s=await(await fetch(e)).arrayBuffer();else if(e instanceof File||e instanceof Blob)s=await yn(e);else return new Uint8Array;return new Uint8Array(s)},jn=async(e,s)=>{var o;const n=await fetch(e);let r;try{const u=parseInt(n.headers.get(gn)||"-1"),p=(o=n.body)==null?void 0:o.getReader();if(!p)throw bn;const g=[];let y=0;for(;;){const{done:m,value:b}=await p.read(),x=b?b.length:0;if(m){if(u!=-1&&u!==y)throw vn;s&&s({url:e,total:u,received:y,delta:x,done:m});break}g.push(b),y+=x,s&&s({url:e,total:u,received:y,delta:x,done:m})}const R=new Uint8Array(y);let d=0;for(const m of g)R.set(m,d),d+=m.length;r=R.buffer}catch(u){console.log("failed to send download progress event: ",u),r=await n.arrayBuffer()}return r},Ya=async(e,s,n=!1,r)=>{const o=n?await jn(e,r):await(await fetch(e)).arrayBuffer(),u=new Blob([o],{type:s});return URL.createObjectURL(u)};let Gt=null,Pe=null,F;async function Nn(){if(Gt)return Gt;if(Pe)return Pe;Pe=(async()=>{const e=new hn;e.on("progress",({progress:o})=>{F==null||F(o,"transcoding")}),F==null||F(.01,"loading_ffmpeg");const s="https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm",n=await Ya(`${s}/ffmpeg-core.js`,"text/javascript");F==null||F(.04,"loading_ffmpeg");const r=await Ya(`${s}/ffmpeg-core.wasm`,"application/wasm");return F==null||F(.07,"loading_ffmpeg"),await e.load({coreURL:n,wasmURL:r}),Gt=e,e})();try{return await Pe}finally{Pe=null}}async function _n(e){const s=URL.createObjectURL(e);try{return await new Promise(r=>{const o=document.createElement("video");o.preload="metadata",o.onloadedmetadata=()=>{const u=Number(o.duration);r(Number.isFinite(u)&&u>0?u:null)},o.onerror=()=>r(null),o.src=s})}finally{URL.revokeObjectURL(s)}}async function Xa(e,s){const n=F;F=s;try{const r=await Nn(),o="input.avi",u="output.mp4";s==null||s(.1,"writing_input"),await r.writeFile(o,await wn(e)),await r.exec(["-i",o,"-c:v","libx264","-preset","veryfast","-crf","24","-pix_fmt","yuv420p","-movflags","+faststart","-an",u]);const p=await r.readFile(u);s==null||s(.98,"packing_output");const g=new Blob([p],{type:"video/mp4"}),y=new File([g],e.name.replace(/\.avi$/i,".mp4"),{type:"video/mp4"});await r.deleteFile(o),await r.deleteFile(u);const R=await _n(y);return s==null||s(1,"done"),{file:y,duration_s:R}}finally{F=n}}function Zt(e){return e==null||Number.isNaN(e)?"—":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(2)} MB`}function $e(e,s){if(!e)return"—";try{const n=new Date(e);return new Intl.DateTimeFormat(s==="en"?"en-GB":"zh-CN",{dateStyle:"short",timeStyle:"medium"}).format(n)}catch{return e}}function de(e){return e==null?null:typeof e=="string"&&e.trim()?e.trim():typeof e=="number"&&!Number.isNaN(e)?String(e):null}function kn(e){return typeof e!="number"||e<=0?null:e>=1e6?`${(e/1e6).toFixed(2)} s`:e>=1e3?`${(e/1e3).toFixed(0)} ms`:`${e} µs`}function Sn(e,s){if(!e)return[];const n=[],r=kn(e.exposure_us);r&&n.push({key:"meta.exposure",value:r});const o=de(e.analogue_gain),u=de(e.digital_gain);if(o||u){const b=[o?`A ${o}`:"",u?`D ${u}`:""].filter(Boolean);n.push({key:"meta.gain",value:b.join(" · ")})}const p=de(e.fps);p&&n.push({key:"meta.fps",value:p});const g=de(e.sensor);g&&n.push({key:"meta.sensor",value:g});const y=de(e.color_mode);y&&n.push({key:"meta.colorMode",value:y});const R=de(e.resolution);R&&n.push({key:"meta.outputResolution",value:R});const d=de(e.modified);d&&n.push({key:"meta.fileTime",value:$e(d,s)});const m=e.size;return typeof m=="number"&&m>0&&n.push({key:"meta.fileSize",value:Zt(m)}),n}function rs(e){const s=n=>typeof n=="number"&&!Number.isNaN(n)?n:null;return e?{tSolveMs:s(e.t_solve_ms),tExtractMs:s(e.t_extract_ms),tPreprocessMs:s(e.t_preprocess_ms),tOpenDecodeMs:s(e.t_open_decode_ms),tBackendTotalMs:s(e.t_backend_total_ms),raDeg:s(e.ra_deg),decDeg:s(e.dec_deg),matches:s(e.matches),rmseArcsec:s(e.rmse_arcsec),prob:s(e.prob),status:typeof e.status=="string"?e.status:null}:{tSolveMs:null,tExtractMs:null,tPreprocessMs:null,tOpenDecodeMs:null,tBackendTotalMs:null,raDeg:null,decDeg:null,matches:null,rmseArcsec:null,prob:null,status:null}}function it(e){return e==null?"—":`${e.toFixed(4)}°`}function ls(e){if(e==null||typeof e!="number"||Number.isNaN(e)||e<0)return null;if(e>=1)return 0;if(e===0)return 100;const s=(1-e)*100;if(s<=100-1e-12)return Math.min(100,Math.max(0,s));const n=-Math.log10(Math.max(e,Number.MIN_VALUE)),r=14,u=Math.min(1,Math.max(0,(n-r)/(50-r)));return Math.min(100,Math.max(0,70+30*u))}function En(e){const s=ls(e);return s==null?"—":`${s.toFixed(1)}%`}function Rn(e){if(e==null)return"—";if(typeof e=="number"&&!Number.isNaN(e)){const s=Math.abs(e);return s>0&&s<.001?e.toExponential(4):s>=0&&s<=1?`${(e*100).toFixed(4)}%`:String(e)}return String(e)}function ye(e,s){const n=s==null?void 0:s.tetra,r=n?n.Prob??n.prob:void 0,o=En(e),u=r!=null?Rn(r):null;return{line:o,rawLine:u}}function nt({result:e,t:s,roundTripMs:n}){const r=rs(e??void 0);return e?t.jsxs("div",{className:"mt-2 space-y-2 text-[10px]",children:[t.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[r.tBackendTotalMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.backendTotalMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tBackendTotalMs.toFixed(0)," ms"]})]}),r.tSolveMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveComputeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tSolveMs.toFixed(0)," ms"]})]}),r.tOpenDecodeMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.openDecodeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tOpenDecodeMs.toFixed(0)," ms"]})]}),r.tPreprocessMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.preprocessMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tPreprocessMs.toFixed(0)," ms"]})]}),r.tExtractMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.extractMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tExtractMs.toFixed(0)," ms"]})]}),n!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveRoundTripMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[n.toFixed(0)," ms"]})]}),r.matches!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.matches")}),t.jsx("div",{className:"font-semibold tabular-nums text-on-surface",children:r.matches})]}),r.rmseArcsec!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.rmse")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.rmseArcsec.toFixed(2),"″"]})]}),r.prob!=null&&t.jsxs("div",{className:"col-span-2",children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.prob")}),t.jsx("div",{className:"font-semibold text-on-surface",children:ye(r.prob,e).line}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/90",children:s("lab.metric.probHelp")}),ye(r.prob,e).rawLine&&t.jsxs("div",{className:"mt-1 text-[9px] text-on-surface-variant",children:[s("lab.metric.probRaw"),": ",ye(r.prob,e).rawLine,t.jsx("p",{className:"mt-0.5 text-[8px] leading-snug opacity-90",children:s("lab.metric.probRawHelp")})]})]})]}),e.centroid_quality!=null&&typeof e.centroid_quality=="object"&&t.jsxs("div",{className:"rounded border border-outline-variant/25 bg-surface-container-high/40 p-2",children:[t.jsxs("div",{className:"text-[9px] font-medium text-on-surface-variant",children:[s("lab.centroidQualityTitle"),typeof e.centroid_quality.level=="number"?` · L${e.centroid_quality.level}`:""]}),Array.isArray(e.centroid_quality.hints)&&e.centroid_quality.hints.length>0?t.jsx("ul",{className:"mt-1 list-inside list-disc text-[9px] leading-snug text-on-surface-variant",children:e.centroid_quality.hints.map((o,u)=>t.jsx("li",{children:o},u))}):null,(()=>{const o=e.centroid_quality.metrics;if(!o)return null;const u=o.input_count,p=o.output_count,g=o.removed_dense,y=o.removed_line;return typeof u!="number"||typeof p!="number"||typeof g!="number"||typeof y!="number"?null:t.jsx("p",{className:"mt-1 font-mono text-[9px] text-on-surface-variant/95",children:s("lab.centroidQualityMetrics",{in:u,out:p,dense:g,line:y})})})()]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.radec")}),t.jsxs("div",{className:"font-mono text-[9px] text-on-surface",children:["α ",it(r.raDeg)," · δ ",it(r.decDeg)]})]}),r.status&&t.jsxs("div",{className:"rounded bg-surface-container-high px-2 py-1 text-[9px] font-mono text-on-surface",children:[s("lab.metric.status"),": ",r.status]})]}):t.jsx("p",{className:"mt-2 text-[10px] text-on-surface-variant",children:"—"})}function z({label:e,helpKey:s,value:n,onChange:r,type:o="number",step:u}){const{t:p}=Te(),g=s?p(s):void 0;return t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e}),g&&t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:g}),t.jsx("input",{type:o,step:u,className:"w-full rounded bg-surface-container-highest px-2 py-1",value:n===""?"":n,onChange:y=>{const R=y.target.value;r(R===""?void 0:Number(R))}})]})}function Mn({view:e,setView:s}){const{t:n,locale:r,setLocale:o}=Te();return t.jsxs("header",{className:"flex h-12 shrink-0 items-center justify-between border-b border-outline-variant/20 px-4",children:[t.jsxs("div",{className:"flex items-center gap-6",children:[t.jsx("span",{className:"font-headline text-sm font-bold tracking-wide text-on-surface",children:n("app.title")}),t.jsxs("nav",{className:"flex flex-wrap gap-3 text-xs",children:[t.jsx("button",{type:"button",className:e==="lab_image"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_image"),children:n("nav.labImage")}),t.jsx("button",{type:"button",className:e==="lab_video"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_video"),children:n("nav.labVideo")}),t.jsx("button",{type:"button",className:e==="pool"?"text-primary":"text-on-surface-variant",onClick:()=>s("pool"),children:n("nav.pool")}),t.jsx("button",{type:"button",className:e==="history"?"text-primary":"text-on-surface-variant",onClick:()=>s("history"),children:n("nav.history")})]})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("zh"),children:n("lang.zh")}),t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("en"),children:n("lang.en")})]}),t.jsxs("a",{className:"flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 text-xs text-on-surface-variant hover:bg-surface-container",href:"/debug",children:[t.jsx(Ks,{className:"h-3.5 w-3.5"})," ",n("nav.systemAdmin")]})]})]})}function Cn({historyQ:e,setHistoryQ:s,historyPage:n,setHistoryPage:r,historyData:o,historyExpandId:u,setHistoryExpandId:p,historyTotalPages:g,onDeleteExperiment:y,onSearch:R}){var b;const{t:d,locale:m}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsx("p",{className:"mb-4 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 p-3 text-xs leading-relaxed text-on-surface-variant",children:d("history.intro")}),t.jsxs("div",{className:"mb-4 flex flex-wrap items-center gap-4",children:[t.jsxs("h2",{className:"flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Gs,{className:"h-5 w-5"})," ",d("history.title")]}),t.jsx("input",{className:"rounded bg-surface-container-highest px-2 py-1 text-xs",placeholder:d("history.search"),value:e,onChange:x=>s(x.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-1 text-xs",onClick:R,children:d("history.searchBtn")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("json").then(x=>{const I=new Blob([x],{type:"application/json"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.json",M.click()}),children:d("history.exportJson")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("csv").then(x=>{const I=new Blob([x],{type:"text/csv"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.csv",M.click()}),children:d("history.exportCsv")})]}),t.jsxs("div",{className:"flex flex-wrap items-center gap-3 text-xs text-on-surface-variant",children:[t.jsx("span",{children:d("history.total",{n:(o==null?void 0:o.total)??0})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n<=1,onClick:()=>r(x=>Math.max(1,x-1)),children:d("history.prev")}),t.jsxs("span",{children:[n," / ",g]}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n>=g,onClick:()=>r(x=>x+1),children:d("history.next")})]}),t.jsx("ul",{className:"mt-2 space-y-2",children:(b=o==null?void 0:o.items)==null?void 0:b.map(x=>{const I=String(x.id??""),M=x.metrics,se=u===I;return t.jsxs("li",{className:"rounded border border-outline-variant/20 p-2 text-[11px]",children:[t.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-2 font-mono",children:[t.jsxs("div",{children:[t.jsxs("div",{className:"text-on-surface",children:[$e(String(x.created_at??""),m)," —"," ",String(x.input_name)," — ",String(x.preset_label)]}),t.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[d("history.preset"),": ",String(x.preset_label)," · ",d("history.metrics"),":"," ","matches=",String((M==null?void 0:M.matches)??"—")," rmse=",String((M==null?void 0:M.rmse_arcsec)??"—")]})]}),t.jsxs("div",{className:"flex shrink-0 gap-1",children:[t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 text-[10px]",onClick:()=>p(se?null:I),children:d(se?"history.collapse":"history.detail")}),t.jsx("button",{type:"button",className:"rounded px-2 py-0.5 text-[10px] text-error hover:bg-error-container/20",title:d("history.delete"),onClick:()=>void y(I),children:d("history.delete")})]})]}),se&&t.jsxs("div",{className:"mt-2 space-y-2",children:[x.asset_snapshot_relpath?t.jsx("img",{src:dn(I),alt:"",className:"max-h-48 max-w-full rounded border border-outline-variant/20 object-contain"}):null,t.jsx("pre",{className:"og-scrollbar max-h-64 overflow-auto rounded bg-surface-container p-2 text-[10px]",children:JSON.stringify(x.result_json,null,2)})]})]},I)})})]})}function In({uploads:e,onDelete:s}){const{t:n,locale:r}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsxs("h2",{className:"mb-4 flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Vs,{className:"h-5 w-5"})," ",n("pool.title")]}),t.jsxs("table",{className:"w-full text-left text-xs",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-outline-variant/30 text-on-surface-variant",children:[t.jsx("th",{className:"py-2",children:n("pool.col.name")}),t.jsx("th",{className:"py-2",children:n("pool.col.source")}),t.jsx("th",{className:"py-2",children:n("pool.col.size")}),t.jsx("th",{className:"py-2",children:n("pool.col.time")}),t.jsx("th",{className:"w-16 py-2 text-center",children:n("pool.delete")})]})}),t.jsx("tbody",{children:e.map(o=>t.jsxs("tr",{className:"border-b border-outline-variant/10",children:[t.jsx("td",{className:"py-2 font-mono",children:o.filename}),t.jsx("td",{className:"py-2",children:o.source??n("common.placeholder")}),t.jsx("td",{className:"py-2",children:Zt(o.size)}),t.jsx("td",{className:"py-2",children:$e(o.modified_at,r)}),t.jsx("td",{className:"py-2 text-center",children:t.jsx("button",{type:"button",className:"inline-flex rounded p-1 text-on-surface-variant hover:bg-error-container/30 hover:text-error",title:n("pool.delete"),"aria-label":n("pool.delete"),onClick:()=>void s(o.filename),children:t.jsx(ss,{className:"h-3.5 w-3.5"})})})]},o.filename))})]})]})}function Kt(e){return e?{matches:e.matches,rmse_arcsec:e.rmse_arcsec,status:e.status,prob:e.prob,t_solve_ms:e.t_solve_ms}:{}}function Ln(e){var n,r;if(!e)return null;const s=e.solve_overlay;return(n=s==null?void 0:s.stars_matched)!=null&&n.length?s.stars_matched.length:(r=s==null?void 0:s.stars_all_centroids)!=null&&r.length?s.stars_all_centroids.length:typeof e.matches=="number"?e.matches:null}function Qa(e){return/\.(jpe?g|png|webp|bmp|gif|fits?)$/i.test(e)}function ge(e){return/\.(mp4|mov|webm|mkv|avi)$/i.test(e)}function rt(e,s=16,n=14){return e.length<=s+n+1?e:`${e.slice(0,s)}...${e.slice(-n)}`}const es=10,lt=30,qt=6,Jt=()=>({hint_ra_deg:45,hint_dec_deg:80,fov_estimate:11,fov_max_error:void 0,solve_timeout_ms:1500,solve_profile:"balanced",max_image_side:1280,large_scale_bg_subtract:!1,detail_level:"summary",centroid_rejection_level:3,centroid:{sigma:2.5,max_area:400,min_area:5,filtsize:25,binary_open:!0,max_axis_ratio:void 0}});function ts(e,s,n,r){const o=typeof r=="number"&&r>0?` ${e("lab.gateNextMs",{ms:r})}`:"",u=String(n??"").toLowerCase();return s==="SKIPPED_BUSY"?u.includes("recording")?`${e("lab.gate.skipRecording")}${o}`:u.includes("previous")?`${e("lab.gate.skipBusyInFlight")}${o}`:`${e("lab.gate.skipBusyFallback",{detail:String(n??"")})}${o}`:s==="SKIPPED_INTERVAL"?`${e("lab.gate.skipInterval")}${o}`:`${s??"?"}${n?`: ${String(n)}`:""}${o}`}async function as(){try{const e=await fetch("/api/dev/debug/camera/stream/status",{cache:"no-store",credentials:"same-origin"});if(!e.ok)return"fail";const s=await e.json();if(s.success===!1)return"fail";const n=Number(s.max_clients??0),r=Number(s.active_clients??0);return n>0&&r>=n?"busy":"ok"}catch{return"fail"}}function Dn(){var La,Da,Oa,Fa,Pa,$a,Ta;const{t:e,locale:s}=Te(),[n,r]=i.useState("lab_image"),[o,u]=i.useState(!1),[p,g]=i.useState(!1),[y,R]=i.useState([]),[d,m]=i.useState(null),[b,x]=i.useState(Jt),[I,M]=i.useState([]),[se,Yt]=i.useState([]),[Xt,is]=i.useState(new Set),[ne,U]=i.useState(!1),[ct,w]=i.useState(null),[k,we]=i.useState(null),[T,je]=i.useState(null),[Ae,Qt]=i.useState([]),[os,ea]=i.useState([]),ta=i.useRef(null),aa=i.useRef(null),[Ne,cs]=i.useState(""),[Ue,sa]=i.useState(1),[Be,dt]=i.useState(null),[na,ra]=i.useState(null),[ut,la]=i.useState(""),[V,ds]=i.useState({matched:!0,pattern:!0,all:!0,rejected:!0}),[He,ia]=i.useState([]),[B,oa]=i.useState(null),[_e,ke]=i.useState(1),[ze,ca]=i.useState(!1),[xe,mt]=i.useState(1),[da,us]=i.useState({w:0,h:0}),[pe,ms]=i.useState({w:0,h:0}),[ua,ma]=i.useState({w:0,h:0}),[S,Se]=i.useState("file"),[xa,We]=i.useState(()=>Date.now()),[xt,G]=i.useState("unknown"),[Ee,pt]=i.useState(null),[Re,pa]=i.useState(!1),[fe,fa]=i.useState(!1),[ha,xs]=i.useState(!1),[K,ft]=i.useState(!1),[ps,ht]=i.useState(null),[bt,vt]=i.useState(null),[gt,Ve]=i.useState(null),[yt,ba]=i.useState(!1),[fs,wt]=i.useState({}),[va,jt]=i.useState(!1),[Nt,J]=i.useState(null),[hs,Me]=i.useState(null),[_t,Ce]=i.useState(null),[kt,ga]=i.useState(null),[ya,wa]=i.useState(!1),[ja,St]=i.useState(null),[bs,X]=i.useState(null),[Na,_a]=i.useState(!1),[Et,re]=i.useState(null),[vs,H]=i.useState(null),[Rt,Mt]=i.useState(null),Ct=i.useRef(null),Ie=i.useRef(null),Le=i.useRef(null),It=i.useRef(0),le=i.useRef(null),ie=i.useRef(null),Lt=i.useRef(!1),Dt=i.useRef(!1),Ot=i.useRef(!1),Ft=i.useRef(!1),he=i.useRef(null),gs=i.useCallback(async()=>{const a=Le.current;if(!a||a.naturalWidth<2)return null;const l=document.createElement("canvas");l.width=a.naturalWidth,l.height=a.naturalHeight;const c=l.getContext("2d");if(!c)return null;try{c.drawImage(a,0,0)}catch{return null}return await new Promise(f=>{l.toBlob(j=>f(j?URL.createObjectURL(j):null),"image/jpeg",.92)})},[]),[Ge,ys]=i.useState(null),[L,ka]=i.useState(null),Ke=i.useCallback((a,l)=>{const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;ea(f=>[...f,{id:c,text:a,variant:l}]),window.setTimeout(()=>{ea(f=>f.filter(j=>j.id!==c))},4800)},[]);i.useEffect(()=>{ta.current=k},[k]),i.useEffect(()=>{aa.current=T},[T]),i.useEffect(()=>{Ot.current=fe},[fe]),i.useEffect(()=>{Ft.current=Re},[Re]),i.useEffect(()=>{ct&&(Ke(ct,"error"),w(null))},[ct,Ke]),i.useEffect(()=>{_t&&(Ke(_t,"warn"),Ce(null))},[_t,Ke]);const qe=i.useCallback(()=>{const a=ta.current,l=aa.current;if(!a&&!l)return;const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;Qt(f=>[{id:c,at:Date.now(),single:a??void 0,batch:l??void 0},...f].slice(0,es))},[]),Je=()=>{ie.current!=null&&(window.clearTimeout(ie.current),ie.current=null)},Ze=()=>{le.current!=null&&(window.clearTimeout(le.current),le.current=null)},Pt=(L==null?void 0:L.star_analysis_min_interval_ms)??2e3,$t=(L==null?void 0:L.star_analysis_max_interval_ms)??12e3,Tt=i.useMemo(()=>{const a=(L==null?void 0:L.star_analysis_target_fps)??.6666666666666666,l=Math.min(Math.max(a,.2),5);return Math.round(1e3/l)},[L]),oe=i.useMemo(()=>Math.min(Math.max(kt??Tt,Pt),$t),[Tt,kt,$t,Pt]),Q=i.useCallback(async()=>{const[a,l,c]=await Promise.all([Js(),Ht("official"),Ht("user")]);R(a.files),M(l.presets),Yt(c.presets)},[]),At=i.useCallback(()=>{an().then(a=>ia(a.files)).catch(()=>ia([]))},[]);i.useEffect(()=>{ln().then(a=>{ka(a);const l=Math.round(1e3/Math.min(Math.max(a.star_analysis_target_fps,.2),5)),c=Math.min(Math.max(l,a.star_analysis_min_interval_ms),a.star_analysis_max_interval_ms);ga(c);const f=a.centroid_rejection_level_default;typeof f=="number"&&f>=1&&f<=5&&x(j=>({...j,centroid_rejection_level:f}))}).catch(()=>ka(null))},[]),i.useEffect(()=>{Q().catch(a=>w(String(a)))},[Q]),i.useEffect(()=>{At()},[At]),i.useEffect(()=>{n==="history"&&Wt(Ne,Ue,lt).then(dt).catch(a=>w(String(a)))},[n,Ne,Ue]),i.useEffect(()=>{if(!d){Ve(null);return}let a=!1;return ba(!0),(async()=>{try{const l=await nn(d);if(a)return;let c={...l};try{c={...await Ka(d),...l}}catch{}Ve(c)}catch{try{const l=await Ka(d);a||Ve(l)}catch{a||Ve(null)}}finally{a||ba(!1)}})(),()=>{a=!0}},[d]),i.useEffect(()=>{we(null),je(null),wt({}),jt(!1),J(null),Me(null),Se("file"),pt(null),Qt([])},[d]);const Z=i.useMemo(()=>{const a=k==null?void 0:k.result;if(!a)return null;const l=a.solve_overlay||null;if(!l)return null;const c=a.overlay_ext||null;return{...l,overlay_ext:c||void 0}},[k]),ee=i.useMemo(()=>(k==null?void 0:k.result)??null,[k]),Sa=i.useMemo(()=>Ln(ee),[ee]),Ut=i.useMemo(()=>n==="lab_image"?da:n==="lab_video"?S==="file"?pe:ua:{w:0,h:0},[n,S,da,pe,ua]),Ye=d?Vt(d):"";i.useEffect(()=>{if(n!=="lab_image")return;const a=Ct.current,l=he.current;if(!a||!l||!Z||!d)return;const c=()=>Ja(l,a,Z,V);a.complete?c():a.onload=c},[Z,d,k,V,n]),i.useEffect(()=>{if(n!=="lab_video"||S!=="file")return;const a=Ie.current,l=he.current;if(!a||!l||!Z||!d)return;const c=()=>mn(l,a,Z,V);return a.addEventListener("loadeddata",c),a.addEventListener("seeked",c),a.readyState>=2&&c(),()=>{a.removeEventListener("loadeddata",c),a.removeEventListener("seeked",c)}},[Z,V,n,S,d,k]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera")return;const a=Le.current,l=he.current;if(!a||!l||!Z)return;const c=()=>Ja(l,a,Z,V);a.complete?c():a.onload=c},[Z,V,n,S,k,xa,K]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera"){G("unknown");return}if(K){G("ok");return}let a=!1;return G("unknown"),(async()=>{try{const l=await as();if(a)return;if(l==="fail"){G("fail");return}if(l==="busy"){G("busy");return}G("ok"),It.current=0,We(Date.now())}catch{a||G("fail")}})(),()=>{a=!0}},[n,S,K]),i.useEffect(()=>{const a=Ct.current;if(!a)return;const l=()=>us({w:a.naturalWidth||0,h:a.naturalHeight||0});return l(),a.addEventListener("load",l),()=>a.removeEventListener("load",l)},[d,Ye]),i.useEffect(()=>{const a=Ie.current;if(!a)return;const l=()=>ms({w:a.videoWidth||0,h:a.videoHeight||0});return a.addEventListener("loadedmetadata",l),a.addEventListener("loadeddata",l),l(),()=>{a.removeEventListener("loadedmetadata",l),a.removeEventListener("loadeddata",l)}},[d,Ye,n]);const ws=a=>{x({...Jt(),...a,centroid:{...Jt().centroid,...a.centroid}})},js=async()=>{if(!d){w(e("err.selectFile"));return}w(null),U(!0);const a=performance.now();try{const l=await Xs(d,b);qe(),we(l),je(null),Me("file"),J(performance.now()-a)}catch(l){w(String(l)),J(null)}finally{U(!1)}},Ns=async()=>{if(!d){w(e("err.selectFile"));return}const a=[];for(const c of Xt){const j=[...I,...se].find($=>$.id===c);j&&a.push({label:j.name,params:structuredClone(j.params)})}if(a.length===0){w(e("err.selectPresets"));return}w(null),U(!0);const l=performance.now();try{const c=await Qs(d,a);qe(),je({results:c.results}),we(null),wt({}),Me("file"),J(performance.now()-l)}catch(c){w(String(c)),J(null)}finally{U(!1)}},Xe=async a=>{if(Lt.current)return;w(null),Lt.current=!0;const l=performance.now();try{const c=await on({source:"camera",overlay_topn_count:3,enable_polar_guide:!0,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),...b}),f=c.gate_status,j=c.next_allowed_in_ms,$=c.effective_interval_ms;if(f==="SKIPPED_INTERVAL"||f==="SKIPPED_BUSY"){if(Ce(ts(e,f,c.gate_reason,typeof j=="number"?j:null)),a){Ze();const C=Math.max(50,Number(j??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}return}Ce(null),c.result!=null&&(qe(),we(c)),je(null),Me("camera"),Se("camera");const E=c.result,D=typeof(E==null?void 0:E.status)=="string"?String(E.status):"";if(ha&&D==="MATCH_FOUND"){ft(!0),ht(c.frame_id!=null?String(c.frame_id):null);const C=await gs();vt(ce=>(ce&&URL.revokeObjectURL(ce),C)),tt()}else if(a){Ze();const C=Math.max(50,Number(j??$??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}J(performance.now()-l)}catch(c){w(String(c)),J(null)}finally{Lt.current=!1}},_s=()=>void Xe(!1),Qe=async a=>{if(Dt.current||!d)return;const l=Ie.current;if(!l||l.videoWidth<2||l.videoHeight<2||Ee)return;w(null),Dt.current=!0;const c=performance.now();try{const f=document.createElement("canvas");f.width=l.videoWidth,f.height=l.videoHeight;const j=f.getContext("2d");if(!j)throw new Error("无法创建画布上下文 / Cannot create canvas context");j.drawImage(l,0,0,f.width,f.height);const $=await new Promise((ve,Ts)=>{f.toBlob(Aa=>Aa?ve(Aa):Ts(new Error("帧编码失败 / Frame encode failed")),"image/jpeg",.92)}),E=await cn($,{...b,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),overlay_topn_count:3,enable_polar_guide:!0}),D=E.gate_status,C=E.next_allowed_in_ms,ce=E.effective_interval_ms;if(D==="SKIPPED_INTERVAL"||D==="SKIPPED_BUSY"){if(Ce(ts(e,D,E.gate_reason,typeof C=="number"?C:null)),a){Je();const ve=Math.max(50,Number(C??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}return}if(Ce(null),E.result!=null&&(qe(),we(E)),je(null),Me("file"),J(performance.now()-c),a){Je();const ve=Math.max(50,Number(C??ce??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}}catch(f){w(String(f)),J(null)}finally{Dt.current=!1}},ks=()=>void Qe(!1),Ss=()=>{fe||!d||(fa(!0),Je(),Qe(!0))},Ea=i.useMemo(()=>n!=="lab_video"||S!=="file"||!d||/\.avi$/i.test(d)||Ee?!1:pe.w>1&&pe.h>1,[n,S,d,Ee,pe.w,pe.h]),Ra=i.useMemo(()=>!!d&&/\.avi$/i.test(d),[d]),Es=async()=>{if(!(!d||!Ra||ya)){w(null),wa(!0),St(0),X(e("lab.transcode.loading"));try{const a=Vt(d),l=await fetch(a,{cache:"no-store"});if(!l.ok)throw new Error(e("lab.transcode.fetchFailed"));const c=await l.blob(),f=new File([c],d,{type:"video/x-msvideo"}),j=await Xa(f,(E,D)=>{St(Math.max(0,Math.min(1,E))),D==="loading_ffmpeg"?X(e("lab.transcode.loading")):D==="writing_input"?X(e("lab.transcode.writingBuffer")):D==="transcoding"?X(e("lab.transcode.running")):D==="packing_output"&&X(e("lab.transcode.packaging"))});X(e("lab.transcode.uploading"));const $=await Bt(j.file,"analysis_transcoded");await Ga({old_filename:d,new_filename:$.filename,duration_s:j.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),await Q(),m($.filename),St(1),X(e("lab.transcode.done"))}catch(a){w(String(a)),X(e("lab.transcode.failed"))}finally{wa(!1)}}},Rs=async a=>{H(e("sidebar.flowDownloadingPool")),re(.19);const l=Vt(a),c=await fetch(l,{cache:"no-store"});if(!c.ok)throw new Error(e("lab.transcode.fetchFailed"));const f=await c.blob(),j=new File([f],a,{type:"video/x-msvideo"}),$=await Xa(j,(D,C)=>{re(.2+Math.max(0,Math.min(1,D))*.6),C==="loading_ffmpeg"?H(e("sidebar.flowLoadingTranscoder")):C==="writing_input"?H(e("sidebar.flowWritingBuffer")):C==="transcoding"?H(e("sidebar.flowTranscoding")):C==="packing_output"&&H(e("sidebar.flowPackaging"))});H(e("sidebar.flowUploadingMp4")),re(.86);const E=await Bt($.file,"debug_console_transcoded");return H(e("sidebar.flowReplacing")),re(.94),await Ga({old_filename:a,new_filename:E.filename,duration_s:$.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),E.filename},Ms=async()=>{if(!B||Na)return;const a=/\.avi$/i.test(B);w(null),_a(!0),re(.02),Mt(null);try{H(e("sidebar.flowImportingDebug"));const l=await Ys(B);re(a?.18:1);let c=l.filename;a?c=await Rs(l.filename):H(e("sidebar.flowNoTranscode")),await Q(),m(c),r(ge(c)?"lab_video":"lab_image"),re(1),H(e("sidebar.flowDone")),Mt(e("sidebar.flowDoneMsg",{name:rt(c)}))}catch(l){w(String(l)),H(e("sidebar.flowFailed")),Mt(String(l))}finally{_a(!1)}},et=()=>{fa(!1),Je()},Cs=()=>{Re||K||(pa(!0),Ze(),Xe(!0))},tt=()=>{pa(!1),Ze()},Is=async()=>{try{const a=Le.current;a&&(a.onload=null,a.onerror=null,a.src="",a.removeAttribute("src")),await new Promise(l=>window.requestAnimationFrame(()=>l())),await fetch("/api/dev/debug/camera/stop",{method:"POST"}),We(Date.now()),S==="camera"&&Se("file")}catch{}},Ls=()=>{ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null)),We(Date.now())},Ds=a=>{is(l=>{const c=new Set(l);return c.has(a)?c.delete(a):c.add(a),c})},Ma=async a=>{if(!window.confirm(e("delete.uploadFirst",{name:a})))return;let l=0;try{l=(await rn(a)).count}catch{l=0}if(l>0){if(!window.confirm(e("delete.uploadCascade",{n:l})))return}else if(!window.confirm(e("delete.uploadSecond")))return;U(!0),w(null);try{await Zs(a,{deleteExperiments:l>0}),await Q(),d===a&&m(null)}catch(c){w(String(c))}finally{U(!1)}},Os=async a=>{if(window.confirm(e("delete.experimentFirst"))&&window.confirm(e("delete.experimentSecond"))){U(!0),w(null);try{await tn(a),na===a&&ra(null);const l=await Wt(Ne,Ue,lt);dt(l)}catch(l){w(String(l))}finally{U(!1)}}},at=a=>mt(Math.min(4,Math.max(.5,a))),Fs=Math.max(1,Math.ceil(((Be==null?void 0:Be.total)??0)/lt)),be=i.useMemo(()=>n==="lab_image"?He.filter(a=>a.type==="image"||Qa(a.name)):n==="lab_video"?He.filter(a=>a.type==="video"||ge(a.name)):He,[He,n]),De=Math.max(1,Math.ceil(be.length/qt)),Ps=i.useMemo(()=>{const a=(_e-1)*qt;return be.slice(a,a+qt)},[be,_e]);i.useEffect(()=>{ke(1)},[n]),i.useEffect(()=>{ke(a=>Math.min(a,De))},[De]),i.useEffect(()=>{B&&!be.some(a=>a.name===B)&&oa(null)},[be,B]);const Ca=i.useMemo(()=>Sn(gt,s),[gt,s]),$s=i.useMemo(()=>n==="lab_image"?y.filter(a=>Qa(a.filename)):n==="lab_video"?y.filter(a=>ge(a.filename)):y,[y,n]),v=i.useMemo(()=>rs(ee),[ee]),Ia=i.useMemo(()=>ls(v.prob),[v.prob]);return i.useEffect(()=>{jt(!1)},[k]),i.useEffect(()=>{if(n!=="lab_video")return;let a;const l=()=>{un().then(ys).catch(()=>{})};return l(),a=setInterval(l,1500),()=>{a&&clearInterval(a)}},[n]),i.useEffect(()=>{(n!=="lab_video"||S!=="camera")&&(tt(),ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null))),(n!=="lab_video"||S!=="file")&&et()},[n,S]),i.useEffect(()=>{d||et()},[d]),i.useEffect(()=>()=>{tt(),et()},[]),t.jsxs("div",{className:"flex h-dvh min-h-0 flex-col overflow-hidden bg-surface text-on-surface",children:[t.jsx(Mn,{view:n,setView:r}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex shrink-0 items-center justify-between gap-2 border-b border-outline-variant/15 bg-surface-container-low px-3 py-2 text-xs 2xl:hidden",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{g(!1),u(!0)},children:s==="zh"?"素材":"Assets"}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{u(!1),g(!0)},children:s==="zh"?"解算参数":"Solve parameters"})]}),(o||p)&&t.jsx("button",{type:"button",className:"fixed inset-x-0 bottom-0 top-[88px] z-40 bg-black/60 2xl:hidden","aria-label":s==="zh"?"关闭面板":"Close panel",onClick:()=>{u(!1),g(!1)}}),t.jsxs("div",{className:"flex min-h-0 flex-1",children:[t.jsxs("aside",{className:`fixed bottom-0 left-0 top-[88px] z-50 w-64 shrink-0 overflow-y-auto border-r border-outline-variant/15 bg-surface-container-lowest p-3 text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${o?"translate-x-0":"-translate-x-full"}`,children:[t.jsx("div",{className:"mb-2 flex justify-end 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>u(!1),children:s==="zh"?"关闭":"Close"})}),t.jsx("div",{className:"mb-3 font-semibold text-on-surface-variant",children:e("sidebar.assets")}),t.jsxs("label",{className:"mb-3 flex cursor-pointer items-center gap-2 rounded bg-primary-container/30 px-2 py-2 text-on-primary-container",children:[t.jsx(qs,{className:"h-4 w-4"}),t.jsx("span",{children:e("sidebar.upload")}),t.jsx("input",{type:"file",accept:"image/*,video/*",className:"hidden",onChange:async a=>{var c;const l=(c=a.target.files)==null?void 0:c[0];if(l){U(!0),w(null);try{const f=await Bt(l);await Q(),m(f.filename)}catch(f){w(String(f))}finally{U(!1),a.target.value=""}}}})]}),t.jsxs("button",{type:"button",className:"mb-2 flex w-full items-center gap-1 text-left text-on-surface-variant hover:text-on-surface",onClick:()=>{Q().catch(a=>w(String(a))),ke(1),At()},children:[t.jsx(Ws,{className:"h-3 w-3"})," ",e("sidebar.refresh")]}),t.jsx("div",{className:"og-scrollbar max-h-36 overflow-y-auto border-t border-outline-variant/10 pt-2",children:$s.map(a=>t.jsxs("div",{className:`mb-1 flex items-center gap-0.5 rounded px-1 ${d===a.filename?"bg-surface-container":""}`,children:[t.jsxs("button",{type:"button",className:`min-w-0 flex-1 truncate rounded px-1 py-1 text-left ${d===a.filename?"text-primary":""}`,title:a.filename,onClick:()=>{m(a.filename),r(ge(a.filename)?"lab_video":"lab_image")},children:[t.jsx("span",{className:"block truncate",children:rt(a.filename)}),t.jsx("span",{className:"block text-[9px] text-on-surface-variant/90",children:ge(a.filename)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")})]}),t.jsx("button",{type:"button",className:"shrink-0 rounded p-1 text-on-surface-variant hover:bg-surface-container-high hover:text-error",title:e("pool.delete"),"aria-label":e("pool.delete"),onClick:l=>{l.stopPropagation(),Ma(a.filename)},children:t.jsx(ss,{className:"h-3.5 w-3.5"})})]},a.filename))}),t.jsxs("div",{className:"mt-3 border-t border-outline-variant/10 pt-3",children:[t.jsxs("div",{className:"mb-2 flex items-start justify-between gap-2",children:[t.jsx("div",{className:"min-w-0 font-semibold leading-tight text-on-surface-variant",children:e("sidebar.debugCaptures")}),t.jsx("button",{type:"button",className:"shrink-0 rounded bg-primary px-2 py-1 text-[10px] font-medium text-on-primary disabled:opacity-40",disabled:!B||Na,title:B??void 0,onClick:()=>void Ms(),children:B&&/\.avi$/i.test(B)?e("sidebar.importToPoolWithTranscode"):e("sidebar.importToPoolDirect")})]}),Et!=null&&t.jsxs("div",{className:"mb-2 rounded border border-outline-variant/25 bg-surface-container-low px-2 py-2",children:[t.jsxs("div",{className:"flex items-center justify-between gap-2 text-[10px]",children:[t.jsx("span",{className:"text-on-surface-variant",children:vs||e("sidebar.flowPreparing")}),t.jsxs("span",{className:"font-mono text-on-surface",children:[Math.round((Et||0)*100),"%"]})]}),t.jsx("div",{className:"mt-1.5 h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round((Et||0)*100)}%`}})}),Rt&&t.jsx("div",{className:"mt-1 text-[10px] text-on-surface-variant",title:Rt,children:Rt})]}),be.length===0?t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("sidebar.debugEmpty")}):t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"og-scrollbar max-h-[min(22rem,55vh)] space-y-1.5 overflow-y-auto pr-0.5",children:Ps.map(a=>t.jsxs("button",{type:"button",className:`flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left transition-colors ${B===a.name?"border-primary bg-primary-container/20":"border-outline-variant/25 hover:bg-surface-container"}`,onClick:()=>oa(a.name),children:[a.type==="image"?t.jsx("img",{src:sn(a.name),alt:"",className:"h-11 w-11 shrink-0 rounded object-cover"}):t.jsx("div",{className:"flex h-11 w-11 shrink-0 items-center justify-center rounded bg-surface-container-high text-[9px] text-on-surface-variant",children:"video"}),t.jsxs("div",{className:"min-w-0 flex-1",children:[t.jsx("div",{className:"truncate font-mono text-[10px] text-on-surface",title:a.name,children:rt(a.name)}),t.jsxs("div",{className:"text-[9px] text-on-surface-variant",children:[a.type==="video"||ge(a.name)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")," ","· ",$e(a.modified,s)," · ",Zt(a.size)]})]})]},a.name))}),t.jsxs("div",{className:"mt-2 flex items-center justify-between gap-1 text-[10px] text-on-surface-variant",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e<=1,onClick:()=>ke(a=>Math.max(1,a-1)),children:e("history.prev")}),t.jsx("span",{className:"tabular-nums",children:e("sidebar.debugPage",{cur:_e,total:De})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e>=De,onClick:()=>ke(a=>Math.min(De,a+1)),children:e("history.next")})]})]})]})]}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1",children:[t.jsxs("main",{className:"og-scrollbar min-h-0 min-w-0 flex-1 overflow-y-auto p-4",children:[n==="lab_video"&&t.jsxs("div",{className:"mb-3 max-w-5xl rounded-lg border border-outline-variant/25 bg-surface-container-low/80 p-3 text-[11px] leading-relaxed text-on-surface",children:[t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.videoLiveIntro")}),t.jsx("p",{className:"mt-1.5 text-[10px] leading-snug text-on-surface-variant/95",children:e("lab.videoMjpegHint")}),t.jsxs("div",{className:"mt-2 flex flex-wrap gap-2",children:[t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="file"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("file"),children:e("lab.previewModeFile")}),t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="camera"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("camera"),children:e("lab.previewModeCamera")}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-[11px] font-medium text-on-surface",onClick:()=>void Is(),children:e("lab.stopCameraPreview")})]})]}),t.jsxs("div",{className:"relative aspect-video w-full max-w-5xl overflow-hidden rounded-lg border border-outline-variant/20 bg-surface-container-lowest",children:[Ia!=null&&t.jsxs("div",{className:"pointer-events-none absolute right-2 top-2 z-20 rounded border border-white/30 bg-black/55 px-2 py-1 text-[10px] font-semibold tabular-nums text-white",children:[e("lab.previewConfidence"),": ",Ia.toFixed(1),"%"]}),n==="lab_video"&&S==="camera"?t.jsx("div",{className:"relative flex h-full min-h-[220px] flex-col items-center justify-center gap-3 bg-black p-2",children:t.jsxs("div",{className:"relative inline-block max-h-[70vh] max-w-full",children:[K&&bt?t.jsx("img",{ref:Le,src:bt,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight})}):K&&!bt?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):xt==="busy"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.videoMjpegHint")})}):xt==="fail"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.cameraStreamStatusFail")})}):xt==="unknown"?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):t.jsx("img",{ref:Le,src:`/api/dev/debug/camera/stream?t=${xa}`,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight}),onError:()=>{(async()=>{const a=await as();if(a==="busy"){G("busy");return}if(a==="fail"){G("fail");return}if(It.current>=6){G("fail");return}It.current+=1,window.setTimeout(()=>We(Date.now()),400)})()}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}):d?t.jsx("div",{className:"og-scrollbar relative h-full min-h-[200px] overflow-auto",children:n==="lab_video"?t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(Ha,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(za,{className:"inline h-3 w-3"})})]}),t.jsxs("div",{className:"flex min-h-[200px] flex-col items-center justify-center gap-2 bg-black py-2",children:[t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("video",{ref:Ie,src:Ye,loop:!0,playsInline:!0,autoPlay:!0,muted:!0,preload:"metadata",className:"max-h-[70vh] w-full max-w-full object-contain",onError:()=>pt(e("lab.videoPreviewFailed")),onLoadedData:()=>{pt(null);const a=Ie.current;a&&a.play().catch(()=>{})}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}),Ee&&t.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-1.5 text-[11px] text-error",children:Ee}),Ra&&t.jsxs("div",{className:"max-w-3xl rounded border border-primary/40 bg-primary/10 px-3 py-2 text-[11px] text-on-surface",children:[t.jsx("div",{className:"font-semibold text-primary",children:e("lab.transcode.title")}),t.jsx("div",{className:"mt-1 text-on-surface-variant",children:e("lab.transcode.desc")}),ja!=null&&t.jsxs("div",{className:"mt-2",children:[t.jsx("div",{className:"mb-1 text-[10px] text-on-surface-variant",children:bs||e("lab.transcode.running")}),t.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round(ja*100)}%`}})})]}),t.jsx("div",{className:"mt-2",children:t.jsx("button",{type:"button",className:"rounded bg-primary px-3 py-1.5 text-[11px] font-medium text-on-primary disabled:opacity-50",disabled:ya,onClick:()=>void Es(),children:e("lab.transcode.button")})})]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(Ha,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(za,{className:"inline h-3 w-3"})})]}),t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("img",{ref:Ct,src:Ye,alt:"preview",className:"max-h-[70vh] w-full object-contain"}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})})]})}):t.jsx("div",{className:"flex h-full items-center justify-center px-4 text-center text-on-surface-variant",children:e(n==="lab_video"?"lab.selectOrUploadVideo":"lab.selectOrUpload")}),ne&&t.jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/40",children:t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"})})]}),(d&&n==="lab_image"||n==="lab_video"&&(S==="file"&&d||S==="camera"))&&t.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 px-3 py-2 text-xs text-on-surface",children:[t.jsx("span",{className:"shrink-0 font-medium text-on-surface-variant",children:e("lab.layers")}),t.jsx("div",{className:"flex flex-wrap gap-x-4 gap-y-1",children:["matched","pattern","all","rejected"].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-1",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:V[a],onChange:l=>ds(c=>({...c,[a]:l.target.checked}))}),t.jsx("span",{children:e(a==="matched"?"lab.layer.matched":a==="pattern"?"lab.layer.pattern":a==="all"?"lab.layer.all":"lab.layer.rejected")})]},a))})]}),(d||n==="lab_video"&&S==="camera")&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 grid gap-2 sm:grid-cols-2",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.solveSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:ee?t.jsxs("div",{className:"space-y-1 text-on-surface",children:[v.tSolveMs!=null&&t.jsxs("div",{className:"space-y-0.5",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveComputeMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[v.tSolveMs.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveComputeHelp")})]}),Nt!=null&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveRoundTripMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[Nt.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveRoundTripHelp")})]}),(v.tBackendTotalMs!=null||v.tOpenDecodeMs!=null||v.tPreprocessMs!=null||v.tExtractMs!=null||v.tSolveMs!=null)&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.backendTotalMs")}),t.jsx("span",{className:"font-mono tabular-nums",children:v.tBackendTotalMs!=null?`${v.tBackendTotalMs.toFixed(0)} ms`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[8px] text-on-surface-variant/90",children:[t.jsxs("span",{children:[e("lab.metric.openDecodeMs"),":"," ",v.tOpenDecodeMs!=null?`${v.tOpenDecodeMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.preprocessMs"),":"," ",v.tPreprocessMs!=null?`${v.tPreprocessMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.extractMs"),":"," ",v.tExtractMs!=null?`${v.tExtractMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.solveOnlyMs"),":"," ",v.tSolveMs!=null?`${v.tSolveMs.toFixed(0)} ms`:e("common.placeholder")]})]})]}),(v.raDeg!=null||v.decDeg!=null)&&t.jsxs("div",{className:"leading-tight",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.radec")}),t.jsxs("div",{className:"mt-0.5 font-mono text-[9px]",children:["α ",it(v.raDeg)," · δ"," ",it(v.decDeg)]})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[9px]",children:[v.matches!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.matches")})," ",t.jsx("span",{className:"font-mono",children:v.matches})]}),v.rmseArcsec!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.rmse")})," ",t.jsxs("span",{className:"font-mono",children:[v.rmseArcsec.toFixed(2),"″"]})]}),v.prob!=null&&t.jsxs("span",{className:"inline-flex max-w-full flex-col gap-0.5",children:[t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.prob")})," ",t.jsx("span",{className:"font-mono",children:ye(v.prob,ee??void 0).line})]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probHelp")}),ye(v.prob,ee??void 0).rawLine&&t.jsxs(t.Fragment,{children:[t.jsxs("span",{className:"text-[8px] text-on-surface-variant",children:[e("lab.metric.probRaw"),":"," ",ye(v.prob,ee??void 0).rawLine]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probRawHelp")})]})]})]}),v.status&&t.jsxs("div",{className:"border-t border-outline-variant/15 pt-1 text-[9px]",children:[t.jsxs("span",{className:"text-on-surface-variant",children:[e("lab.metric.status")," "]}),t.jsx("span",{className:"font-mono text-secondary",children:v.status})]})]}):t.jsx("p",{className:"text-on-surface-variant",children:e("common.placeholder")})})]}),t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.imageSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.resolution")}),t.jsx("span",{className:"font-mono tabular-nums",children:Ut.w>0?`${Ut.w}×${Ut.h}`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.starsDetected")}),t.jsx("span",{className:"font-mono tabular-nums",children:Sa??e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.fwhm")}),t.jsx("span",{children:e("common.placeholder")})]})]})]})]}),d&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 flex flex-wrap gap-4 text-xs text-on-surface-variant",children:[t.jsxs("span",{children:[e("lab.file"),":"," ",t.jsx("span",{className:"font-mono text-on-surface",title:d,children:rt(d,22,18)})]}),((La=y.find(a=>a.filename===d))==null?void 0:La.source)&&t.jsxs("span",{className:"rounded bg-surface-container-high px-2 py-0.5",children:[e("lab.source"),":"," ",(Da=y.find(a=>a.filename===d))==null?void 0:Da.source]})]}),t.jsxs("details",{open:!0,className:"mt-2 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 text-xs shadow-sm",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center gap-2 px-3 py-2 font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.meta.title"),yt&&t.jsx(st,{className:"h-3.5 w-3.5 animate-spin"}),t.jsx(Fe,{className:"ml-auto h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:Ca.length>0?t.jsx("dl",{className:"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-3",children:Ca.map(a=>t.jsxs("div",{children:[t.jsx("dt",{className:"text-[10px] text-on-surface-variant",children:e(a.key)}),t.jsx("dd",{className:"text-[11px] font-medium text-on-surface",children:a.value})]},`${a.key}-${a.value}`))}):gt&&!yt?t.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:e("lab.meta.partial")}):yt?null:t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.meta.noSidecar")})})]})]})]}),(d||n==="lab_video"&&S==="camera")&&(Ae.length>0||k||T)&&t.jsxs(t.Fragment,{children:[Ae.length>0&&t.jsxs("section",{className:"mt-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.solveHistoryTitle")}),t.jsxs("span",{className:"ml-2 normal-case text-on-surface-variant/70",children:["(",Ae.length,"/",es,")"]})]}),t.jsx("div",{className:"og-scrollbar max-h-[min(36vh,18rem)] space-y-2 overflow-y-auto p-3",children:Ae.map(a=>{var $;const l=a.batch,c=a.single,f=$e(new Date(a.at).toISOString(),s),j=l&&l.results.length?e("results.historyBatch",{count:l.results.length}):e("results.historySingle");return t.jsxs("details",{className:"rounded border border-outline-variant/20 bg-surface-container/80 text-xs",children:[t.jsx("summary",{className:"cursor-pointer list-none px-3 py-2 font-medium text-on-surface [&::-webkit-details-marker]:hidden",children:t.jsxs("span",{children:[f," · ",j]})}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[($=l==null?void 0:l.results)!=null&&$.length?t.jsx("div",{className:"og-scrollbar flex gap-2 overflow-x-auto pb-1",children:l.results.map((E,D)=>{const C=E.result,ce=E.success===!0;return t.jsxs("div",{className:"min-w-[12rem] max-w-xs shrink-0 rounded border border-outline-variant/20 bg-surface-container-lowest p-2",children:[t.jsx("div",{className:"border-b border-outline-variant/10 pb-1 text-[10px] font-semibold text-secondary",children:String(E.label??D)}),ce&&C?t.jsx(nt,{result:C,t:e,roundTripMs:null}):t.jsx("div",{className:"mt-1 text-[10px] text-error",children:String(E.error??"—")})]},D)})}):null,c&&c.result?t.jsx("div",{className:"mt-2 rounded border border-outline-variant/15 p-2",children:t.jsx(nt,{result:c.result,t:e,roundTripMs:null})}):null,t.jsxs("details",{className:"mt-2",children:[t.jsx("summary",{className:"cursor-pointer text-[10px] text-primary hover:underline",children:e("results.viewRaw")}),t.jsx("pre",{className:"og-scrollbar mt-1 max-h-32 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify({single:c,batch:l},null,2)})]})]})]},a.id)})})]}),(k||T)&&t.jsxs("section",{className:"mt-3 max-h-[min(50vh,28rem)] rounded-lg border border-outline-variant/25 bg-surface-container-lowest/95",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center justify-between border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.title")}),t.jsxs("div",{className:"flex gap-3",children:[(Oa=T==null?void 0:T.results)!=null&&Oa.length?t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!(!d||!T)){for(const a of T.results){if(!a.success)continue;const l=a.result;await zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(l??null),replay:{layers:V,params:b}})}w(null)}},children:e("results.saveBatchAll")}):null,k&&t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!d&&hs!=="camera")return;const a=k.result;await zt({input_name:d??e("lab.cameraSnapshotName"),preset_label:"manual",result_json:k,metrics:Kt(a??null),replay:{layers:V,params:b}}),w(null)},children:e("results.saveCurrent")})]})]}),t.jsx("div",{className:"og-scrollbar min-h-0 overflow-y-auto p-3",children:t.jsxs("div",{className:"og-scrollbar flex gap-3 overflow-x-auto text-xs",children:[T==null?void 0:T.results.map((a,l)=>{const c=a.result,f=fs[l]??!1;return t.jsxs("div",{className:"min-w-[15rem] max-w-sm shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 font-semibold text-secondary",children:String(a.label)}),a.success?t.jsxs(t.Fragment,{children:[t.jsx(nt,{result:c,t:e,roundTripMs:null}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>wt(j=>({...j,[l]:!f})),children:e(f?"results.hideRaw":"results.viewRaw")}),f&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-40 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(a,null,2)})]}):t.jsx("div",{className:"mt-2 text-[10px] text-error",children:String(a.error)}),a.success===!0&&d?t.jsx("button",{type:"button",className:"mt-3 w-full rounded bg-surface-container-high py-1.5 text-[10px] font-medium",onClick:()=>zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(c??null),replay:{layers:V,params:b}}).catch(j=>w(String(j))),children:e("results.saveRow")}):null]},l)}),k&&!T&&t.jsxs("div",{className:"min-w-[16rem] max-w-md shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 text-[10px] font-semibold uppercase text-on-surface-variant",children:e("results.title")}),t.jsx(nt,{result:k.result,t:e,roundTripMs:Nt}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>jt(a=>!a),children:e(va?"results.hideRaw":"results.viewRaw")}),va&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-48 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(k,null,2)})]})]})})]})]})]}),t.jsxs("aside",{className:`fixed bottom-0 right-0 top-[88px] z-50 flex min-h-0 w-80 shrink-0 flex-col border-l border-outline-variant/15 bg-surface-container-low text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${p?"translate-x-0":"translate-x-full"}`,children:[t.jsx("div",{className:"flex shrink-0 justify-end border-b border-outline-variant/15 p-2 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>g(!1),children:s==="zh"?"关闭":"Close"})}),n!=="lab_video"&&t.jsxs("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container",onClick:js,disabled:ne,children:e("btn.solveOne")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary",onClick:Ns,disabled:ne,children:e("btn.solveBatch")})]}),n==="lab_video"&&t.jsx("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:S==="file"?t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Ss(),disabled:ne||!Ea||fe,children:e("lab.solveFileStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>et(),disabled:!fe,children:e("lab.solveFileStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void ks(),disabled:ne||!Ea||fe,children:e("lab.solveCurrentFrame")})]}):t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Cs(),disabled:ne||Re||K,children:e("lab.solveCameraStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>tt(),disabled:!Re,children:e("lab.solveCameraStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void _s(),disabled:ne||K,children:e("lab.solveCameraFrame")}),t.jsxs("label",{className:"flex items-center gap-2 rounded border border-outline-variant/25 px-2 py-1.5 text-[11px]",children:[t.jsx("input",{type:"checkbox",checked:ha,onChange:a=>xs(a.target.checked)}),t.jsx("span",{children:"自动保持(解算成功后冻结) / Auto Hold"})]}),K&&t.jsx("button",{type:"button",className:"w-full rounded bg-tertiary/80 py-2.5 font-semibold text-on-tertiary disabled:opacity-40",onClick:()=>Ls(),children:"继续实时 / Resume Live"}),K&&t.jsxs("div",{className:"text-[10px] text-on-surface-variant",children:["已冻结帧 / Frozen frame ",ps??"-"]})]})}),n==="lab_video"&&Ge&&t.jsxs("div",{className:"shrink-0 border-b border-outline-variant/20 px-4 py-2 text-[10px] text-on-surface-variant",children:[t.jsx("div",{className:"font-medium text-on-surface",children:e("lab.systemLoad")}),t.jsxs("div",{children:["CPU ",Ge.cpu_usage,"% · RAM ",Ge.memory_usage,"% ·"," ",Ge.temperature,"°C"]})]}),t.jsxs("div",{className:"og-scrollbar min-h-0 flex-1 overflow-y-auto p-4",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.title"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockSolveIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.solveProfile")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.solveProfileHelp")}),t.jsxs("select",{className:"w-full rounded bg-surface-container-highest px-2 py-1",value:b.solve_profile??"balanced",onChange:a=>x(l=>({...l,solve_profile:a.target.value})),children:[t.jsx("option",{value:"speed",children:e("params.solveProfileSpeed")}),t.jsx("option",{value:"balanced",children:e("params.solveProfileBalanced")}),t.jsx("option",{value:"robust",children:e("params.solveProfileRobust")})]})]}),t.jsx(z,{label:e("params.fov"),helpKey:"params.fovHelp",value:b.fov_estimate??"",onChange:a=>x(l=>({...l,fov_estimate:a}))}),t.jsx(z,{label:e("params.fovErr"),helpKey:"params.fovErrHelp",value:b.fov_max_error??"",onChange:a=>x(l=>({...l,fov_max_error:a}))}),t.jsx(z,{label:e("params.timeout"),helpKey:"params.timeoutHelp",value:b.solve_timeout_ms??"",onChange:a=>x(l=>({...l,solve_timeout_ms:a}))}),t.jsx(z,{label:e("params.solveIntervalMs"),helpKey:"params.solveIntervalMsHelp",value:kt??"",onChange:a=>ga(a==null?Tt:Math.round(a))}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalBound",{min:Pt,max:$t,effective:oe})}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalIndependent")}),n==="lab_video"&&t.jsxs("label",{className:"mt-2 block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.centroidRejectionLevel")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.centroidRejectionLevelHelp")}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"1"}),t.jsx("input",{type:"range",min:1,max:5,step:1,className:"h-2 flex-1 accent-primary",value:b.centroid_rejection_level??3,onChange:a=>x(l=>({...l,centroid_rejection_level:Number(a.target.value)}))}),t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"5"}),t.jsx("span",{className:"ml-1 min-w-[1.25rem] tabular-nums text-[10px] font-semibold text-on-surface",children:b.centroid_rejection_level??3})]}),t.jsx("p",{className:"mt-0.5 text-[9px] text-on-surface-variant/90",children:e("params.centroidRejectionScale")})]}),t.jsx(z,{label:e("params.ra"),helpKey:"params.raHelp",value:b.hint_ra_deg??"",onChange:a=>x(l=>({...l,hint_ra_deg:a}))}),t.jsx(z,{label:e("params.dec"),helpKey:"params.decHelp",value:b.hint_dec_deg??"",onChange:a=>x(l=>({...l,hint_dec_deg:a}))}),t.jsx(z,{label:e("params.maxSide"),helpKey:"params.maxSideHelp",value:b.max_image_side??"",onChange:a=>x(l=>({...l,max_image_side:a}))}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:b.detail_level==="full",onChange:a=>x(l=>({...l,detail_level:a.target.checked?"full":"summary"}))}),t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.detailLevelFull")})]}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-start gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary mt-0.5",checked:!!b.large_scale_bg_subtract,onChange:a=>x(l=>({...l,large_scale_bg_subtract:a.target.checked}))}),t.jsxs("span",{children:[t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.largeScaleBg")}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.largeScaleBgHelp")})]})]})]})]})]}),t.jsxs("details",{open:!0,className:"mt-3 rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.centroid"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockCentroidIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsx(z,{label:e("params.sigma"),helpKey:"params.sigmaHelp",step:.1,value:((Fa=b.centroid)==null?void 0:Fa.sigma)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,sigma:a}}))}),t.jsx(z,{label:e("params.maxArea"),helpKey:"params.maxAreaHelp",value:((Pa=b.centroid)==null?void 0:Pa.max_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,max_area:a}}))}),t.jsx(z,{label:e("params.minArea"),helpKey:"params.minAreaHelp",value:(($a=b.centroid)==null?void 0:$a.min_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,min_area:a}}))}),t.jsx(z,{label:e("params.filtsize"),helpKey:"params.filtsizeHelp",step:2,value:((Ta=b.centroid)==null?void 0:Ta.filtsize)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,filtsize:a}}))})]})]})]}),t.jsxs("div",{className:"mt-4 rounded-md border border-outline-variant/20 bg-surface-container-highest/40 p-3",children:[t.jsx("div",{className:"mb-2 font-semibold text-on-surface-variant",children:e("sidebar.batchPresets")}),t.jsx("p",{className:"mb-2 text-[10px] leading-snug text-on-surface-variant",children:e("sidebar.batchHint")}),t.jsx("div",{className:"og-scrollbar max-h-36 space-y-1.5 overflow-y-auto",children:[...I,...se].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",checked:Xt.has(a.id),onChange:()=>Ds(a.id)}),t.jsx("span",{className:"truncate",children:a.name})]},a.id))})]}),t.jsxs("div",{className:"mt-6 border-t border-outline-variant/20 pt-4",children:[t.jsx("div",{className:"mb-2 font-semibold",children:e("btn.applyPresets")}),t.jsx("div",{className:"og-scrollbar max-h-24 space-y-1 overflow-y-auto",children:[...I,...se].map(a=>t.jsx("button",{type:"button",className:"block w-full truncate text-left text-primary hover:underline",onClick:()=>ws(a.params),children:a.name},a.id))}),t.jsxs("div",{className:"mt-3 flex gap-1",children:[t.jsx("input",{className:"flex-1 rounded bg-surface-container-highest px-2 py-1",placeholder:e("placeholder.newPreset"),value:ut,onChange:a=>la(a.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container-high px-2",onClick:async()=>{if(ut.trim()){U(!0);try{await en(ut.trim(),b),la(""),await Q();const a=await Ht("user");Yt(a.presets)}catch(a){w(String(a))}finally{U(!1)}}},children:e("btn.savePreset")})]})]})]})]})]}),n==="pool"&&t.jsx(In,{uploads:y,onDelete:a=>void Ma(a)}),n==="history"&&t.jsx(Cn,{historyQ:Ne,setHistoryQ:cs,historyPage:Ue,setHistoryPage:sa,historyData:Be,historyExpandId:na,setHistoryExpandId:ra,historyTotalPages:Fs,onDeleteExperiment:a=>void Os(a),onSearch:()=>Wt(Ne,1,lt).then(a=>{sa(1),dt(a)})})]}),t.jsx("div",{className:"pointer-events-none fixed bottom-4 right-4 z-[100] flex max-w-sm flex-col gap-2",children:os.map(a=>t.jsx("div",{className:`pointer-events-auto rounded border px-3 py-2 text-left text-xs shadow-lg ${a.variant==="error"?"border-error/50 bg-error-container/95 text-on-error-container":a.variant==="warn"?"border-amber-500/40 bg-surface-container-highest/95 text-on-surface":"border-outline-variant/40 bg-surface-container-high/95 text-on-surface"}`,children:a.text},a.id))})]})}Bs.createRoot(document.getElementById("root")).render(t.jsx(Hs.StrictMode,{children:t.jsx(zs,{children:t.jsx(Dn,{})})})); + */const Va=Y("ZoomOut",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65",key:"13gj7c"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11",key:"durymu"}]]),_="/api/dev";async function W(e){if((e.headers.get("content-type")||"").includes("application/json"))return e.json();const n=await e.text();throw new Error(n||`HTTP ${e.status}`)}async function Js(){const e=await fetch(`${_}/analysis/uploads`);if(!e.ok)throw new Error(await e.text());return e.json()}async function Zs(e,s){const n=new URLSearchParams;s!=null&&s.deleteExperiments&&n.set("delete_experiments","true");const r=n.toString(),o=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}${r?`?${r}`:""}`,{method:"DELETE"});if(!o.ok)throw new Error(await o.text());return await o.json().catch(()=>({}))}async function Bt(e,s="analysis_upload"){const n=new FormData;n.append("file",e),n.append("source",s);const r=await fetch(`${_}/analysis/upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}async function Ys(e){const s=await fetch(`${_}/analysis/uploads/import_from_debug`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({filename:e})}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Ga(e){const s=await fetch(`${_}/analysis/uploads/replace_video`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function Xs(e,s){const n=await fetch(`${_}/analysis/solve/image`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,...s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Qs(e,s){const n=await fetch(`${_}/analysis/solve/batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input_name:e,runs:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function Ht(e){const s=await fetch(`${_}/analysis/presets?scope=${e}`);if(!s.ok)throw new Error(await s.text());return s.json()}async function en(e,s){const n=await fetch(`${_}/analysis/presets`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,params:s})}),r=await W(n);if(!n.ok)throw new Error(String(r.detail||n.status));return r}async function zt(e){const s=await fetch(`${_}/analysis/experiments`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function tn(e){const s=await fetch(`${_}/analysis/experiments/${encodeURIComponent(e)}`,{method:"DELETE"});if(!s.ok)throw new Error(await s.text())}async function Wt(e,s,n=30){const r=new URLSearchParams({page:String(s),page_size:String(n)});e&&r.set("q",e);const o=await fetch(`${_}/analysis/experiments?${r}`);if(!o.ok)throw new Error(await o.text());return o.json()}async function an(){const e=await fetch(`${_}/debug/files`);if(!e.ok)throw new Error(await e.text());return e.json()}function sn(e){return`${_}/debug/files/${encodeURIComponent(e)}`}async function Ka(e){const s=await fetch(`${_}/debug/files/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function nn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/info`),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}function Vt(e){return`${_}/analysis/uploads/file?filename=${encodeURIComponent(e)}`}async function qa(e){const s=await fetch(`${_}/analysis/experiments/export?format=${e}`);if(!s.ok)throw new Error(await s.text());return s.text()}async function rn(e){const s=await fetch(`${_}/analysis/uploads/${encodeURIComponent(e)}/experiment_count`);if(!s.ok)throw new Error(await s.text());return s.json()}async function ln(){const e=await fetch(`${_}/analysis/settings`);if(!e.ok)throw new Error(await e.text());return e.json()}async function on(e){const s=await fetch(`${_}/analysis/solve/frame`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),n=await W(s);if(!s.ok)throw new Error(String(n.detail||s.status));return n}async function cn(e,s){const n=new FormData;n.append("file",e,"frame.jpg"),n.append("payload",JSON.stringify(s));const r=await fetch(`${_}/analysis/solve/frame_upload`,{method:"POST",body:n}),o=await W(r);if(!r.ok)throw new Error(String(o.detail||r.status));return o}function dn(e){return`${_}/analysis/experiments/${encodeURIComponent(e)}/asset`}async function un(){const e=await fetch("/api/dev/system/info");if(!e.ok)throw new Error(await e.text());return e.json()}function ns(e,s,n,r,o){var g,y,R,d;if(!r)return;if(e.clearRect(0,0,s,n),o.all&&Array.isArray(r.stars_all_centroids)){e.fillStyle="rgba(156, 163, 175, 0.85)";for(const m of r.stars_all_centroids)e.beginPath(),e.arc(m.x,m.y,2.4,0,Math.PI*2),e.fill()}if(o.rejected&&Array.isArray(r.stars_rejected_centroids)){e.strokeStyle="rgba(248, 113, 113, 0.95)",e.lineWidth=1.6;for(const m of r.stars_rejected_centroids)e.beginPath(),e.moveTo(m.x-4,m.y-4),e.lineTo(m.x+4,m.y+4),e.moveTo(m.x+4,m.y-4),e.lineTo(m.x-4,m.y+4),e.stroke()}if(o.pattern&&Array.isArray(r.stars_pattern)){e.strokeStyle="rgba(251, 146, 60, 0.95)",e.lineWidth=2;for(const m of r.stars_pattern)e.beginPath(),e.arc(m.x,m.y,6,0,Math.PI*2),e.stroke()}if(o.matched&&Array.isArray(r.stars_matched)){e.strokeStyle="rgba(34, 197, 94, 0.95)",e.fillStyle="rgba(34, 197, 94, 0.95)",e.lineWidth=2,e.font="11px system-ui, sans-serif";for(const m of r.stars_matched)e.beginPath(),e.arc(m.x,m.y,7,0,Math.PI*2),e.stroke(),m.mag!=null&&e.fillText(`m${Number(m.mag).toFixed(1)}`,m.x+4,m.y-4)}const u=r.overlay_ext;if(Array.isArray(u==null?void 0:u.labels_topn)&&u.labels_topn.length>0){e.fillStyle="rgba(96, 165, 250, 0.95)",e.font="12px system-ui, sans-serif";for(const m of u.labels_topn){if(typeof(m==null?void 0:m.x)!="number"||typeof(m==null?void 0:m.y)!="number")continue;const b=typeof m.mag=="number"?` m${m.mag.toFixed(1)}`:"",x=`${m.name??"Star"}${b}`;e.fillText(x,m.x+8,m.y+14)}}const p=u==null?void 0:u.polar_guide;if(p&&typeof((g=p.frame_center)==null?void 0:g.x)=="number"&&typeof((y=p.frame_center)==null?void 0:y.y)=="number"&&typeof((R=p.target)==null?void 0:R.x)=="number"&&typeof((d=p.target)==null?void 0:d.y)=="number"){const m=p.frame_center.x,b=p.frame_center.y,x=p.target.x,I=p.target.y;e.strokeStyle="rgba(244, 63, 94, 0.95)",e.fillStyle="rgba(244, 63, 94, 0.95)",e.lineWidth=2,e.beginPath(),e.moveTo(m,b),e.lineTo(x,I),e.stroke(),e.beginPath(),e.arc(x,I,6,0,Math.PI*2),e.stroke();const M=typeof p.angular_sep_deg=="number"?p.angular_sep_deg.toFixed(2):"-";e.font="12px system-ui, sans-serif",e.fillText(`Polar ${M}deg`,x+10,I-6)}}function Ja(e,s,n,r){if(!n)return;const o=s.naturalWidth||1,u=s.naturalHeight||1;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}function mn(e,s,n,r){if(!n)return;const o=s.videoWidth||1,u=s.videoHeight||1;if(o<2||u<2)return;e.width=o,e.height=u,e.style.width=`${s.clientWidth}px`,e.style.height=`${s.clientHeight}px`;const p=e.getContext("2d");p&&ns(p,o,u,n,r)}var N;(function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"})(N||(N={}));const xn=(()=>{let e=0;return()=>e++})(),pn=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),fn=new Error("called FFmpeg.terminate()");var A,ae,Z,ue,me,ot,P;class hn{constructor(){te(this,A,null);te(this,ae,{});te(this,Z,{});te(this,ue,[]);te(this,me,[]);O(this,"loaded",!1);te(this,ot,()=>{h(this,A)&&(h(this,A).onmessage=({data:{id:s,type:n,data:r}})=>{switch(n){case N.LOAD:this.loaded=!0,h(this,ae)[s](r);break;case N.MOUNT:case N.UNMOUNT:case N.EXEC:case N.FFPROBE:case N.WRITE_FILE:case N.READ_FILE:case N.DELETE_FILE:case N.RENAME:case N.CREATE_DIR:case N.LIST_DIR:case N.DELETE_DIR:h(this,ae)[s](r);break;case N.LOG:h(this,ue).forEach(o=>o(r));break;case N.PROGRESS:h(this,me).forEach(o=>o(r));break;case N.ERROR:h(this,Z)[s](r);break}delete h(this,ae)[s],delete h(this,Z)[s]})});te(this,P,({type:s,data:n},r=[],o)=>h(this,A)?new Promise((u,p)=>{const g=xn();h(this,A)&&h(this,A).postMessage({id:g,type:s,data:n},r),h(this,ae)[g]=u,h(this,Z)[g]=p,o==null||o.addEventListener("abort",()=>{p(new DOMException(`Message # ${g} was aborted`,"AbortError"))},{once:!0})}):Promise.reject(pn));O(this,"load",({classWorkerURL:s,...n}={},{signal:r}={})=>(h(this,A)||(Oe(this,A,s?new Worker(new URL(s,import.meta.url),{type:"module"}):new Worker(new URL("/static/analysis-lab/assets/worker-BAOIWoxA.js",import.meta.url),{type:"module"})),h(this,ot).call(this)),h(this,P).call(this,{type:N.LOAD,data:n},void 0,r)));O(this,"exec",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.EXEC,data:{args:s,timeout:n}},void 0,r));O(this,"ffprobe",(s,n=-1,{signal:r}={})=>h(this,P).call(this,{type:N.FFPROBE,data:{args:s,timeout:n}},void 0,r));O(this,"terminate",()=>{const s=Object.keys(h(this,Z));for(const n of s)h(this,Z)[n](fn),delete h(this,Z)[n],delete h(this,ae)[n];h(this,A)&&(h(this,A).terminate(),Oe(this,A,null),this.loaded=!1)});O(this,"writeFile",(s,n,{signal:r}={})=>{const o=[];return n instanceof Uint8Array&&o.push(n.buffer),h(this,P).call(this,{type:N.WRITE_FILE,data:{path:s,data:n}},o,r)});O(this,"mount",(s,n,r)=>{const o=[];return h(this,P).call(this,{type:N.MOUNT,data:{fsType:s,options:n,mountPoint:r}},o)});O(this,"unmount",s=>{const n=[];return h(this,P).call(this,{type:N.UNMOUNT,data:{mountPoint:s}},n)});O(this,"readFile",(s,n="binary",{signal:r}={})=>h(this,P).call(this,{type:N.READ_FILE,data:{path:s,encoding:n}},void 0,r));O(this,"deleteFile",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_FILE,data:{path:s}},void 0,n));O(this,"rename",(s,n,{signal:r}={})=>h(this,P).call(this,{type:N.RENAME,data:{oldPath:s,newPath:n}},void 0,r));O(this,"createDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.CREATE_DIR,data:{path:s}},void 0,n));O(this,"listDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.LIST_DIR,data:{path:s}},void 0,n));O(this,"deleteDir",(s,{signal:n}={})=>h(this,P).call(this,{type:N.DELETE_DIR,data:{path:s}},void 0,n))}on(s,n){s==="log"?h(this,ue).push(n):s==="progress"&&h(this,me).push(n)}off(s,n){s==="log"?Oe(this,ue,h(this,ue).filter(r=>r!==n)):s==="progress"&&Oe(this,me,h(this,me).filter(r=>r!==n))}}A=new WeakMap,ae=new WeakMap,Z=new WeakMap,ue=new WeakMap,me=new WeakMap,ot=new WeakMap,P=new WeakMap;var Za;(function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"})(Za||(Za={}));const bn=new Error("failed to get response body reader"),vn=new Error("failed to complete download"),gn="Content-Length",yn=e=>new Promise((s,n)=>{const r=new FileReader;r.onload=()=>{const{result:o}=r;o instanceof ArrayBuffer?s(new Uint8Array(o)):s(new Uint8Array)},r.onerror=o=>{var u,p;n(Error(`File could not be read! Code=${((p=(u=o==null?void 0:o.target)==null?void 0:u.error)==null?void 0:p.code)||-1}`))},r.readAsArrayBuffer(e)}),wn=async e=>{let s;if(typeof e=="string")/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?s=atob(e.split(",")[1]).split("").map(n=>n.charCodeAt(0)):s=await(await fetch(e)).arrayBuffer();else if(e instanceof URL)s=await(await fetch(e)).arrayBuffer();else if(e instanceof File||e instanceof Blob)s=await yn(e);else return new Uint8Array;return new Uint8Array(s)},jn=async(e,s)=>{var o;const n=await fetch(e);let r;try{const u=parseInt(n.headers.get(gn)||"-1"),p=(o=n.body)==null?void 0:o.getReader();if(!p)throw bn;const g=[];let y=0;for(;;){const{done:m,value:b}=await p.read(),x=b?b.length:0;if(m){if(u!=-1&&u!==y)throw vn;s&&s({url:e,total:u,received:y,delta:x,done:m});break}g.push(b),y+=x,s&&s({url:e,total:u,received:y,delta:x,done:m})}const R=new Uint8Array(y);let d=0;for(const m of g)R.set(m,d),d+=m.length;r=R.buffer}catch(u){console.log("failed to send download progress event: ",u),r=await n.arrayBuffer()}return r},Ya=async(e,s,n=!1,r)=>{const o=n?await jn(e,r):await(await fetch(e)).arrayBuffer(),u=new Blob([o],{type:s});return URL.createObjectURL(u)};let Gt=null,Pe=null,F;async function Nn(){if(Gt)return Gt;if(Pe)return Pe;Pe=(async()=>{const e=new hn;e.on("progress",({progress:o})=>{F==null||F(o,"transcoding")}),F==null||F(.01,"loading_ffmpeg");const s="https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm",n=await Ya(`${s}/ffmpeg-core.js`,"text/javascript");F==null||F(.04,"loading_ffmpeg");const r=await Ya(`${s}/ffmpeg-core.wasm`,"application/wasm");return F==null||F(.07,"loading_ffmpeg"),await e.load({coreURL:n,wasmURL:r}),Gt=e,e})();try{return await Pe}finally{Pe=null}}async function _n(e){const s=URL.createObjectURL(e);try{return await new Promise(r=>{const o=document.createElement("video");o.preload="metadata",o.onloadedmetadata=()=>{const u=Number(o.duration);r(Number.isFinite(u)&&u>0?u:null)},o.onerror=()=>r(null),o.src=s})}finally{URL.revokeObjectURL(s)}}async function Xa(e,s){const n=F;F=s;try{const r=await Nn(),o="input.avi",u="output.mp4";s==null||s(.1,"writing_input"),await r.writeFile(o,await wn(e)),await r.exec(["-i",o,"-c:v","libx264","-preset","veryfast","-crf","24","-pix_fmt","yuv420p","-movflags","+faststart","-an",u]);const p=await r.readFile(u);s==null||s(.98,"packing_output");const g=new Blob([p],{type:"video/mp4"}),y=new File([g],e.name.replace(/\.avi$/i,".mp4"),{type:"video/mp4"});await r.deleteFile(o),await r.deleteFile(u);const R=await _n(y);return s==null||s(1,"done"),{file:y,duration_s:R}}finally{F=n}}function Zt(e){return e==null||Number.isNaN(e)?"—":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(2)} MB`}function $e(e,s){if(!e)return"—";try{const n=new Date(e);return new Intl.DateTimeFormat(s==="en"?"en-GB":"zh-CN",{dateStyle:"short",timeStyle:"medium"}).format(n)}catch{return e}}function de(e){return e==null?null:typeof e=="string"&&e.trim()?e.trim():typeof e=="number"&&!Number.isNaN(e)?String(e):null}function kn(e){return typeof e!="number"||e<=0?null:e>=1e6?`${(e/1e6).toFixed(2)} s`:e>=1e3?`${(e/1e3).toFixed(0)} ms`:`${e} µs`}function Sn(e,s){if(!e)return[];const n=[],r=kn(e.exposure_us);r&&n.push({key:"meta.exposure",value:r});const o=de(e.analogue_gain),u=de(e.digital_gain);if(o||u){const b=[o?`A ${o}`:"",u?`D ${u}`:""].filter(Boolean);n.push({key:"meta.gain",value:b.join(" · ")})}const p=de(e.fps);p&&n.push({key:"meta.fps",value:p});const g=de(e.sensor);g&&n.push({key:"meta.sensor",value:g});const y=de(e.color_mode);y&&n.push({key:"meta.colorMode",value:y});const R=de(e.resolution);R&&n.push({key:"meta.outputResolution",value:R});const d=de(e.modified);d&&n.push({key:"meta.fileTime",value:$e(d,s)});const m=e.size;return typeof m=="number"&&m>0&&n.push({key:"meta.fileSize",value:Zt(m)}),n}function rs(e){const s=n=>typeof n=="number"&&!Number.isNaN(n)?n:null;return e?{tSolveMs:s(e.t_solve_ms),tExtractMs:s(e.t_extract_ms),tPreprocessMs:s(e.t_preprocess_ms),tOpenDecodeMs:s(e.t_open_decode_ms),tBackendTotalMs:s(e.t_backend_total_ms),raDeg:s(e.ra_deg),decDeg:s(e.dec_deg),matches:s(e.matches),rmseArcsec:s(e.rmse_arcsec),prob:s(e.prob),status:typeof e.status=="string"?e.status:null}:{tSolveMs:null,tExtractMs:null,tPreprocessMs:null,tOpenDecodeMs:null,tBackendTotalMs:null,raDeg:null,decDeg:null,matches:null,rmseArcsec:null,prob:null,status:null}}function it(e){return e==null?"—":`${e.toFixed(4)}°`}function ls(e){if(e==null||typeof e!="number"||Number.isNaN(e)||e<0)return null;if(e>=1)return 0;if(e===0)return 100;const s=(1-e)*100;if(s<=100-1e-12)return Math.min(100,Math.max(0,s));const n=-Math.log10(Math.max(e,Number.MIN_VALUE)),r=14,u=Math.min(1,Math.max(0,(n-r)/(50-r)));return Math.min(100,Math.max(0,70+30*u))}function En(e){const s=ls(e);return s==null?"—":`${s.toFixed(1)}%`}function Rn(e){if(e==null)return"—";if(typeof e=="number"&&!Number.isNaN(e)){const s=Math.abs(e);return s>0&&s<.001?e.toExponential(4):s>=0&&s<=1?`${(e*100).toFixed(4)}%`:String(e)}return String(e)}function ye(e,s){const n=s==null?void 0:s.tetra,r=n?n.Prob??n.prob:void 0,o=En(e),u=r!=null?Rn(r):null;return{line:o,rawLine:u}}function nt({result:e,t:s,roundTripMs:n}){const r=rs(e??void 0);return e?t.jsxs("div",{className:"mt-2 space-y-2 text-[10px]",children:[t.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[r.tBackendTotalMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.backendTotalMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tBackendTotalMs.toFixed(0)," ms"]})]}),r.tSolveMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveComputeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tSolveMs.toFixed(0)," ms"]})]}),r.tOpenDecodeMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.openDecodeMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tOpenDecodeMs.toFixed(0)," ms"]})]}),r.tPreprocessMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.preprocessMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tPreprocessMs.toFixed(0)," ms"]})]}),r.tExtractMs!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.extractMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.tExtractMs.toFixed(0)," ms"]})]}),n!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.solveRoundTripMs")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[n.toFixed(0)," ms"]})]}),r.matches!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.matches")}),t.jsx("div",{className:"font-semibold tabular-nums text-on-surface",children:r.matches})]}),r.rmseArcsec!=null&&t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.rmse")}),t.jsxs("div",{className:"font-semibold tabular-nums text-on-surface",children:[r.rmseArcsec.toFixed(2),"″"]})]}),r.prob!=null&&t.jsxs("div",{className:"col-span-2",children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.prob")}),t.jsx("div",{className:"font-semibold text-on-surface",children:ye(r.prob,e).line}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/90",children:s("lab.metric.probHelp")}),ye(r.prob,e).rawLine&&t.jsxs("div",{className:"mt-1 text-[9px] text-on-surface-variant",children:[s("lab.metric.probRaw"),": ",ye(r.prob,e).rawLine,t.jsx("p",{className:"mt-0.5 text-[8px] leading-snug opacity-90",children:s("lab.metric.probRawHelp")})]})]})]}),e.centroid_quality!=null&&typeof e.centroid_quality=="object"&&t.jsxs("div",{className:"rounded border border-outline-variant/25 bg-surface-container-high/40 p-2",children:[t.jsxs("div",{className:"text-[9px] font-medium text-on-surface-variant",children:[s("lab.centroidQualityTitle"),typeof e.centroid_quality.level=="number"?` · L${e.centroid_quality.level}`:""]}),Array.isArray(e.centroid_quality.hints)&&e.centroid_quality.hints.length>0?t.jsx("ul",{className:"mt-1 list-inside list-disc text-[9px] leading-snug text-on-surface-variant",children:e.centroid_quality.hints.map((o,u)=>t.jsx("li",{children:o},u))}):null,(()=>{const o=e.centroid_quality.metrics;if(!o)return null;const u=o.input_count,p=o.output_count,g=o.removed_dense,y=o.removed_line;return typeof u!="number"||typeof p!="number"||typeof g!="number"||typeof y!="number"?null:t.jsx("p",{className:"mt-1 font-mono text-[9px] text-on-surface-variant/95",children:s("lab.centroidQualityMetrics",{in:u,out:p,dense:g,line:y})})})()]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-on-surface-variant",children:s("lab.metric.radec")}),t.jsxs("div",{className:"font-mono text-[9px] text-on-surface",children:["α ",it(r.raDeg)," · δ ",it(r.decDeg)]})]}),r.status&&t.jsxs("div",{className:"rounded bg-surface-container-high px-2 py-1 text-[9px] font-mono text-on-surface",children:[s("lab.metric.status"),": ",r.status]})]}):t.jsx("p",{className:"mt-2 text-[10px] text-on-surface-variant",children:"—"})}function z({label:e,helpKey:s,value:n,onChange:r,type:o="number",step:u}){const{t:p}=Te(),g=s?p(s):void 0;return t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e}),g&&t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:g}),t.jsx("input",{type:o,step:u,className:"w-full rounded bg-surface-container-highest px-2 py-1",value:n===""?"":n,onChange:y=>{const R=y.target.value;r(R===""?void 0:Number(R))}})]})}function Mn({view:e,setView:s}){const{t:n,locale:r,setLocale:o}=Te();return t.jsxs("header",{className:"flex h-12 shrink-0 items-center justify-between border-b border-outline-variant/20 px-4",children:[t.jsxs("div",{className:"flex items-center gap-6",children:[t.jsx("span",{className:"font-headline text-sm font-bold tracking-wide text-on-surface",children:n("app.title")}),t.jsxs("nav",{className:"flex flex-wrap gap-3 text-xs",children:[t.jsx("button",{type:"button",className:e==="lab_image"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_image"),children:n("nav.labImage")}),t.jsx("button",{type:"button",className:e==="lab_video"?"text-primary":"text-on-surface-variant",onClick:()=>s("lab_video"),children:n("nav.labVideo")}),t.jsx("button",{type:"button",className:e==="pool"?"text-primary":"text-on-surface-variant",onClick:()=>s("pool"),children:n("nav.pool")}),t.jsx("button",{type:"button",className:e==="history"?"text-primary":"text-on-surface-variant",onClick:()=>s("history"),children:n("nav.history")})]})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("div",{className:"mr-2 flex gap-1 text-[10px]",children:[t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("zh"),children:n("lang.zh")}),t.jsx("button",{type:"button",className:`rounded px-2 py-0.5 ${r==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>o("en"),children:n("lang.en")})]}),t.jsxs("a",{className:"flex items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 text-xs text-on-surface-variant hover:bg-surface-container",href:"/debug",children:[t.jsx(Ks,{className:"h-3.5 w-3.5"})," ",n("nav.systemAdmin")]})]})]})}function Cn({historyQ:e,setHistoryQ:s,historyPage:n,setHistoryPage:r,historyData:o,historyExpandId:u,setHistoryExpandId:p,historyTotalPages:g,onDeleteExperiment:y,onSearch:R}){var b;const{t:d,locale:m}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsx("p",{className:"mb-4 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 p-3 text-xs leading-relaxed text-on-surface-variant",children:d("history.intro")}),t.jsxs("div",{className:"mb-4 flex flex-wrap items-center gap-4",children:[t.jsxs("h2",{className:"flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Gs,{className:"h-5 w-5"})," ",d("history.title")]}),t.jsx("input",{className:"rounded bg-surface-container-highest px-2 py-1 text-xs",placeholder:d("history.search"),value:e,onChange:x=>s(x.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-1 text-xs",onClick:R,children:d("history.searchBtn")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("json").then(x=>{const I=new Blob([x],{type:"application/json"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.json",M.click()}),children:d("history.exportJson")}),t.jsx("button",{type:"button",className:"rounded bg-primary-container/50 px-2 py-1 text-xs",onClick:()=>qa("csv").then(x=>{const I=new Blob([x],{type:"text/csv"}),M=document.createElement("a");M.href=URL.createObjectURL(I),M.download="experiments.csv",M.click()}),children:d("history.exportCsv")})]}),t.jsxs("div",{className:"flex flex-wrap items-center gap-3 text-xs text-on-surface-variant",children:[t.jsx("span",{children:d("history.total",{n:(o==null?void 0:o.total)??0})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n<=1,onClick:()=>r(x=>Math.max(1,x-1)),children:d("history.prev")}),t.jsxs("span",{children:[n," / ",g]}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:n>=g,onClick:()=>r(x=>x+1),children:d("history.next")})]}),t.jsx("ul",{className:"mt-2 space-y-2",children:(b=o==null?void 0:o.items)==null?void 0:b.map(x=>{const I=String(x.id??""),M=x.metrics,se=u===I;return t.jsxs("li",{className:"rounded border border-outline-variant/20 p-2 text-[11px]",children:[t.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-2 font-mono",children:[t.jsxs("div",{children:[t.jsxs("div",{className:"text-on-surface",children:[$e(String(x.created_at??""),m)," —"," ",String(x.input_name)," — ",String(x.preset_label)]}),t.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[d("history.preset"),": ",String(x.preset_label)," · ",d("history.metrics"),":"," ","matches=",String((M==null?void 0:M.matches)??"—")," rmse=",String((M==null?void 0:M.rmse_arcsec)??"—")]})]}),t.jsxs("div",{className:"flex shrink-0 gap-1",children:[t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 text-[10px]",onClick:()=>p(se?null:I),children:d(se?"history.collapse":"history.detail")}),t.jsx("button",{type:"button",className:"rounded px-2 py-0.5 text-[10px] text-error hover:bg-error-container/20",title:d("history.delete"),onClick:()=>void y(I),children:d("history.delete")})]})]}),se&&t.jsxs("div",{className:"mt-2 space-y-2",children:[x.asset_snapshot_relpath?t.jsx("img",{src:dn(I),alt:"",className:"max-h-48 max-w-full rounded border border-outline-variant/20 object-contain"}):null,t.jsx("pre",{className:"og-scrollbar max-h-64 overflow-auto rounded bg-surface-container p-2 text-[10px]",children:JSON.stringify(x.result_json,null,2)})]})]},I)})})]})}function In({uploads:e,onDelete:s}){const{t:n,locale:r}=Te();return t.jsxs("main",{className:"og-scrollbar flex-1 overflow-auto p-4",children:[t.jsxs("h2",{className:"mb-4 flex items-center gap-2 text-lg font-semibold",children:[t.jsx(Vs,{className:"h-5 w-5"})," ",n("pool.title")]}),t.jsxs("table",{className:"w-full text-left text-xs",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-outline-variant/30 text-on-surface-variant",children:[t.jsx("th",{className:"py-2",children:n("pool.col.name")}),t.jsx("th",{className:"py-2",children:n("pool.col.source")}),t.jsx("th",{className:"py-2",children:n("pool.col.size")}),t.jsx("th",{className:"py-2",children:n("pool.col.time")}),t.jsx("th",{className:"w-16 py-2 text-center",children:n("pool.delete")})]})}),t.jsx("tbody",{children:e.map(o=>t.jsxs("tr",{className:"border-b border-outline-variant/10",children:[t.jsx("td",{className:"py-2 font-mono",children:o.filename}),t.jsx("td",{className:"py-2",children:o.source??n("common.placeholder")}),t.jsx("td",{className:"py-2",children:Zt(o.size)}),t.jsx("td",{className:"py-2",children:$e(o.modified_at,r)}),t.jsx("td",{className:"py-2 text-center",children:t.jsx("button",{type:"button",className:"inline-flex rounded p-1 text-on-surface-variant hover:bg-error-container/30 hover:text-error",title:n("pool.delete"),"aria-label":n("pool.delete"),onClick:()=>void s(o.filename),children:t.jsx(ss,{className:"h-3.5 w-3.5"})})})]},o.filename))})]})]})}function Kt(e){return e?{matches:e.matches,rmse_arcsec:e.rmse_arcsec,status:e.status,prob:e.prob,t_solve_ms:e.t_solve_ms}:{}}function Ln(e){var n,r;if(!e)return null;const s=e.solve_overlay;return(n=s==null?void 0:s.stars_matched)!=null&&n.length?s.stars_matched.length:(r=s==null?void 0:s.stars_all_centroids)!=null&&r.length?s.stars_all_centroids.length:typeof e.matches=="number"?e.matches:null}function Qa(e){return/\.(jpe?g|png|webp|bmp|gif|fits?)$/i.test(e)}function ge(e){return/\.(mp4|mov|webm|mkv|avi)$/i.test(e)}function rt(e,s=16,n=14){return e.length<=s+n+1?e:`${e.slice(0,s)}...${e.slice(-n)}`}const es=10,lt=30,qt=6,Jt=()=>({hint_ra_deg:45,hint_dec_deg:80,fov_estimate:11,fov_max_error:void 0,solve_timeout_ms:1500,solve_profile:"balanced",max_image_side:1280,large_scale_bg_subtract:!1,detail_level:"summary",centroid_rejection_level:3,centroid:{sigma:2.5,max_area:400,min_area:5,filtsize:25,binary_open:!0,max_axis_ratio:void 0}});function ts(e,s,n,r){const o=typeof r=="number"&&r>0?` ${e("lab.gateNextMs",{ms:r})}`:"",u=String(n??"").toLowerCase();return s==="SKIPPED_BUSY"?u.includes("recording")?`${e("lab.gate.skipRecording")}${o}`:u.includes("previous")?`${e("lab.gate.skipBusyInFlight")}${o}`:`${e("lab.gate.skipBusyFallback",{detail:String(n??"")})}${o}`:s==="SKIPPED_INTERVAL"?`${e("lab.gate.skipInterval")}${o}`:`${s??"?"}${n?`: ${String(n)}`:""}${o}`}async function as(){try{const e=await fetch("/api/dev/debug/camera/stream/status",{cache:"no-store",credentials:"same-origin"});if(!e.ok)return"fail";const s=await e.json();if(s.success===!1)return"fail";const n=Number(s.max_clients??0),r=Number(s.active_clients??0);return n>0&&r>=n?"busy":"ok"}catch{return"fail"}}function Dn(){var La,Da,Oa,Fa,Pa,$a,Ta;const{t:e,locale:s}=Te(),[n,r]=i.useState("lab_image"),[o,u]=i.useState(!1),[p,g]=i.useState(!1),[y,R]=i.useState([]),[d,m]=i.useState(null),[b,x]=i.useState(Jt),[I,M]=i.useState([]),[se,Yt]=i.useState([]),[Xt,is]=i.useState(new Set),[ne,U]=i.useState(!1),[ct,w]=i.useState(null),[k,we]=i.useState(null),[T,je]=i.useState(null),[Ae,Qt]=i.useState([]),[os,ea]=i.useState([]),ta=i.useRef(null),aa=i.useRef(null),[Ne,cs]=i.useState(""),[Ue,sa]=i.useState(1),[Be,dt]=i.useState(null),[na,ra]=i.useState(null),[ut,la]=i.useState(""),[V,ds]=i.useState({matched:!0,pattern:!0,all:!0,rejected:!0}),[He,ia]=i.useState([]),[B,oa]=i.useState(null),[_e,ke]=i.useState(1),[ze,ca]=i.useState(!1),[xe,mt]=i.useState(1),[da,us]=i.useState({w:0,h:0}),[pe,ms]=i.useState({w:0,h:0}),[ua,ma]=i.useState({w:0,h:0}),[S,Se]=i.useState("file"),[xa,We]=i.useState(()=>Date.now()),[xt,G]=i.useState("unknown"),[Ee,pt]=i.useState(null),[Re,pa]=i.useState(!1),[fe,fa]=i.useState(!1),[ha,xs]=i.useState(!1),[K,ft]=i.useState(!1),[ps,ht]=i.useState(null),[bt,vt]=i.useState(null),[gt,Ve]=i.useState(null),[yt,ba]=i.useState(!1),[fs,wt]=i.useState({}),[va,jt]=i.useState(!1),[Nt,q]=i.useState(null),[hs,Me]=i.useState(null),[_t,Ce]=i.useState(null),[kt,ga]=i.useState(null),[ya,wa]=i.useState(!1),[ja,St]=i.useState(null),[bs,X]=i.useState(null),[Na,_a]=i.useState(!1),[Et,re]=i.useState(null),[vs,H]=i.useState(null),[Rt,Mt]=i.useState(null),Ct=i.useRef(null),Ie=i.useRef(null),Le=i.useRef(null),It=i.useRef(0),le=i.useRef(null),ie=i.useRef(null),Lt=i.useRef(!1),Dt=i.useRef(!1),Ot=i.useRef(!1),Ft=i.useRef(!1),he=i.useRef(null),gs=i.useCallback(async()=>{const a=Le.current;if(!a||a.naturalWidth<2)return null;const l=document.createElement("canvas");l.width=a.naturalWidth,l.height=a.naturalHeight;const c=l.getContext("2d");if(!c)return null;try{c.drawImage(a,0,0)}catch{return null}return await new Promise(f=>{l.toBlob(j=>f(j?URL.createObjectURL(j):null),"image/jpeg",.92)})},[]),[Ge,ys]=i.useState(null),[L,ka]=i.useState(null),Ke=i.useCallback((a,l)=>{const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;ea(f=>[...f,{id:c,text:a,variant:l}]),window.setTimeout(()=>{ea(f=>f.filter(j=>j.id!==c))},4800)},[]);i.useEffect(()=>{ta.current=k},[k]),i.useEffect(()=>{aa.current=T},[T]),i.useEffect(()=>{Ot.current=fe},[fe]),i.useEffect(()=>{Ft.current=Re},[Re]),i.useEffect(()=>{ct&&(Ke(ct,"error"),w(null))},[ct,Ke]),i.useEffect(()=>{_t&&(Ke(_t,"warn"),Ce(null))},[_t,Ke]);const qe=i.useCallback(()=>{const a=ta.current,l=aa.current;if(!a&&!l)return;const c=`${Date.now()}-${Math.random().toString(36).slice(2,9)}`;Qt(f=>[{id:c,at:Date.now(),single:a??void 0,batch:l??void 0},...f].slice(0,es))},[]),Je=()=>{ie.current!=null&&(window.clearTimeout(ie.current),ie.current=null)},Ze=()=>{le.current!=null&&(window.clearTimeout(le.current),le.current=null)},Pt=(L==null?void 0:L.star_analysis_min_interval_ms)??2e3,$t=(L==null?void 0:L.star_analysis_max_interval_ms)??12e3,Tt=i.useMemo(()=>{const a=(L==null?void 0:L.star_analysis_target_fps)??.6666666666666666,l=Math.min(Math.max(a,.2),5);return Math.round(1e3/l)},[L]),oe=i.useMemo(()=>Math.min(Math.max(kt??Tt,Pt),$t),[Tt,kt,$t,Pt]),Q=i.useCallback(async()=>{const[a,l,c]=await Promise.all([Js(),Ht("official"),Ht("user")]);R(a.files),M(l.presets),Yt(c.presets)},[]),At=i.useCallback(()=>{an().then(a=>ia(a.files)).catch(()=>ia([]))},[]);i.useEffect(()=>{ln().then(a=>{ka(a);const l=Math.round(1e3/Math.min(Math.max(a.star_analysis_target_fps,.2),5)),c=Math.min(Math.max(l,a.star_analysis_min_interval_ms),a.star_analysis_max_interval_ms);ga(c);const f=a.centroid_rejection_level_default;typeof f=="number"&&f>=1&&f<=5&&x(j=>({...j,centroid_rejection_level:f}))}).catch(()=>ka(null))},[]),i.useEffect(()=>{Q().catch(a=>w(String(a)))},[Q]),i.useEffect(()=>{At()},[At]),i.useEffect(()=>{n==="history"&&Wt(Ne,Ue,lt).then(dt).catch(a=>w(String(a)))},[n,Ne,Ue]),i.useEffect(()=>{if(!d){Ve(null);return}let a=!1;return ba(!0),(async()=>{try{const l=await nn(d);if(a)return;let c={...l};try{c={...await Ka(d),...l}}catch{}Ve(c)}catch{try{const l=await Ka(d);a||Ve(l)}catch{a||Ve(null)}}finally{a||ba(!1)}})(),()=>{a=!0}},[d]),i.useEffect(()=>{we(null),je(null),wt({}),jt(!1),q(null),Me(null),Se("file"),pt(null),Qt([])},[d]);const J=i.useMemo(()=>{const a=k==null?void 0:k.result;if(!a)return null;const l=a.solve_overlay||null;if(!l)return null;const c=a.overlay_ext||null;return{...l,overlay_ext:c||void 0}},[k]),ee=i.useMemo(()=>(k==null?void 0:k.result)??null,[k]),Sa=i.useMemo(()=>Ln(ee),[ee]),Ut=i.useMemo(()=>n==="lab_image"?da:n==="lab_video"?S==="file"?pe:ua:{w:0,h:0},[n,S,da,pe,ua]),Ye=d?Vt(d):"";i.useEffect(()=>{if(n!=="lab_image")return;const a=Ct.current,l=he.current;if(!a||!l||!J||!d)return;const c=()=>Ja(l,a,J,V);a.complete?c():a.onload=c},[J,d,k,V,n]),i.useEffect(()=>{if(n!=="lab_video"||S!=="file")return;const a=Ie.current,l=he.current;if(!a||!l||!J||!d)return;const c=()=>mn(l,a,J,V);return a.addEventListener("loadeddata",c),a.addEventListener("seeked",c),a.readyState>=2&&c(),()=>{a.removeEventListener("loadeddata",c),a.removeEventListener("seeked",c)}},[J,V,n,S,d,k]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera")return;const a=Le.current,l=he.current;if(!a||!l||!J)return;const c=()=>Ja(l,a,J,V);a.complete?c():a.onload=c},[J,V,n,S,k,xa,K]),i.useEffect(()=>{if(n!=="lab_video"||S!=="camera"){G("unknown");return}if(K){G("ok");return}let a=!1;return G("unknown"),(async()=>{try{const l=await as();if(a)return;if(l==="fail"){G("fail");return}if(l==="busy"){G("busy");return}G("ok"),It.current=0,We(Date.now())}catch{a||G("fail")}})(),()=>{a=!0}},[n,S,K]),i.useEffect(()=>{const a=Ct.current;if(!a)return;const l=()=>us({w:a.naturalWidth||0,h:a.naturalHeight||0});return l(),a.addEventListener("load",l),()=>a.removeEventListener("load",l)},[d,Ye]),i.useEffect(()=>{const a=Ie.current;if(!a)return;const l=()=>ms({w:a.videoWidth||0,h:a.videoHeight||0});return a.addEventListener("loadedmetadata",l),a.addEventListener("loadeddata",l),l(),()=>{a.removeEventListener("loadedmetadata",l),a.removeEventListener("loadeddata",l)}},[d,Ye,n]);const ws=a=>{x({...Jt(),...a,centroid:{...Jt().centroid,...a.centroid}})},js=async()=>{if(!d){w(e("err.selectFile"));return}w(null),U(!0);const a=performance.now();try{const l=await Xs(d,b);qe(),we(l),je(null),Me("file"),q(performance.now()-a)}catch(l){w(String(l)),q(null)}finally{U(!1)}},Ns=async()=>{if(!d){w(e("err.selectFile"));return}const a=[];for(const c of Xt){const j=[...I,...se].find($=>$.id===c);j&&a.push({label:j.name,params:structuredClone(j.params)})}if(a.length===0){w(e("err.selectPresets"));return}w(null),U(!0);const l=performance.now();try{const c=await Qs(d,a);qe(),je({results:c.results}),we(null),wt({}),Me("file"),q(performance.now()-l)}catch(c){w(String(c)),q(null)}finally{U(!1)}},Xe=async a=>{if(Lt.current)return;w(null),Lt.current=!0;const l=performance.now();try{const c=await on({source:"camera",overlay_topn_count:3,enable_polar_guide:!0,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),...b}),f=c.gate_status,j=c.next_allowed_in_ms,$=c.effective_interval_ms;if(f==="SKIPPED_INTERVAL"||f==="SKIPPED_BUSY"){if(Ce(ts(e,f,c.gate_reason,typeof j=="number"?j:null)),a){Ze();const C=Math.max(50,Number(j??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}return}Ce(null),c.result!=null&&(qe(),we(c)),je(null),Me("camera"),Se("camera");const E=c.result,D=typeof(E==null?void 0:E.status)=="string"?String(E.status):"";if(ha&&D==="MATCH_FOUND"){ft(!0),ht(c.frame_id!=null?String(c.frame_id):null);const C=await gs();vt(ce=>(ce&&URL.revokeObjectURL(ce),C)),tt()}else if(a){Ze();const C=Math.max(50,Number(j??$??oe));le.current=window.setTimeout(()=>{le.current=null,Ft.current&&Xe(!0)},C)}q(performance.now()-l)}catch(c){w(String(c)),q(null)}finally{Lt.current=!1}},_s=()=>void Xe(!1),Qe=async a=>{if(Dt.current||!d)return;const l=Ie.current;if(!l||l.videoWidth<2||l.videoHeight<2||Ee)return;w(null),Dt.current=!0;const c=performance.now();try{const f=document.createElement("canvas");f.width=l.videoWidth,f.height=l.videoHeight;const j=f.getContext("2d");if(!j)throw new Error("无法创建画布上下文 / Cannot create canvas context");j.drawImage(l,0,0,f.width,f.height);const $=await new Promise((ve,Ts)=>{f.toBlob(Aa=>Aa?ve(Aa):Ts(new Error("帧编码失败 / Frame encode failed")),"image/jpeg",.92)}),E=await cn($,{...b,solve_interval_ms:oe,solve_timeout_ms:Math.min(((L==null?void 0:L.solver_timeout_ms)??1500)*.6,1200),overlay_topn_count:3,enable_polar_guide:!0}),D=E.gate_status,C=E.next_allowed_in_ms,ce=E.effective_interval_ms;if(D==="SKIPPED_INTERVAL"||D==="SKIPPED_BUSY"){if(Ce(ts(e,D,E.gate_reason,typeof C=="number"?C:null)),a){Je();const ve=Math.max(50,Number(C??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}return}if(Ce(null),E.result!=null&&(qe(),we(E)),je(null),Me("file"),q(performance.now()-c),a){Je();const ve=Math.max(50,Number(C??ce??oe));ie.current=window.setTimeout(()=>{ie.current=null,Ot.current&&Qe(!0)},ve)}}catch(f){w(String(f)),q(null)}finally{Dt.current=!1}},ks=()=>void Qe(!1),Ss=()=>{fe||!d||(fa(!0),Je(),Qe(!0))},Ea=i.useMemo(()=>n!=="lab_video"||S!=="file"||!d||/\.avi$/i.test(d)||Ee?!1:pe.w>1&&pe.h>1,[n,S,d,Ee,pe.w,pe.h]),Ra=i.useMemo(()=>!!d&&/\.avi$/i.test(d),[d]),Es=async()=>{if(!(!d||!Ra||ya)){w(null),wa(!0),St(0),X(e("lab.transcode.loading"));try{const a=Vt(d),l=await fetch(a,{cache:"no-store"});if(!l.ok)throw new Error(e("lab.transcode.fetchFailed"));const c=await l.blob(),f=new File([c],d,{type:"video/x-msvideo"}),j=await Xa(f,(E,D)=>{St(Math.max(0,Math.min(1,E))),D==="loading_ffmpeg"?X(e("lab.transcode.loading")):D==="writing_input"?X(e("lab.transcode.writingBuffer")):D==="transcoding"?X(e("lab.transcode.running")):D==="packing_output"&&X(e("lab.transcode.packaging"))});X(e("lab.transcode.uploading"));const $=await Bt(j.file,"analysis_transcoded");await Ga({old_filename:d,new_filename:$.filename,duration_s:j.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),await Q(),m($.filename),St(1),X(e("lab.transcode.done"))}catch(a){w(String(a)),X(e("lab.transcode.failed"))}finally{wa(!1)}}},Rs=async a=>{H(e("sidebar.flowDownloadingPool")),re(.19);const l=Vt(a),c=await fetch(l,{cache:"no-store"});if(!c.ok)throw new Error(e("lab.transcode.fetchFailed"));const f=await c.blob(),j=new File([f],a,{type:"video/x-msvideo"}),$=await Xa(j,(D,C)=>{re(.2+Math.max(0,Math.min(1,D))*.6),C==="loading_ffmpeg"?H(e("sidebar.flowLoadingTranscoder")):C==="writing_input"?H(e("sidebar.flowWritingBuffer")):C==="transcoding"?H(e("sidebar.flowTranscoding")):C==="packing_output"&&H(e("sidebar.flowPackaging"))});H(e("sidebar.flowUploadingMp4")),re(.86);const E=await Bt($.file,"debug_console_transcoded");return H(e("sidebar.flowReplacing")),re(.94),await Ga({old_filename:a,new_filename:E.filename,duration_s:$.duration_s,nominal_fps:null,codec_fourcc:"libx264",container:"MP4"}),E.filename},Ms=async()=>{if(!B||Na)return;const a=/\.avi$/i.test(B);w(null),_a(!0),re(.02),Mt(null);try{H(e("sidebar.flowImportingDebug"));const l=await Ys(B);re(a?.18:1);let c=l.filename;a?c=await Rs(l.filename):H(e("sidebar.flowNoTranscode")),await Q(),m(c),r(ge(c)?"lab_video":"lab_image"),re(1),H(e("sidebar.flowDone")),Mt(e("sidebar.flowDoneMsg",{name:rt(c)}))}catch(l){w(String(l)),H(e("sidebar.flowFailed")),Mt(String(l))}finally{_a(!1)}},et=()=>{fa(!1),Je()},Cs=()=>{Re||K||(pa(!0),Ze(),Xe(!0))},tt=()=>{pa(!1),Ze()},Is=async()=>{try{const a=Le.current;a&&(a.onload=null,a.onerror=null,a.src="",a.removeAttribute("src")),await new Promise(l=>window.requestAnimationFrame(()=>l())),await fetch("/api/dev/debug/camera/stop",{method:"POST"}),We(Date.now()),S==="camera"&&Se("file")}catch{}},Ls=()=>{ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null)),We(Date.now())},Ds=a=>{is(l=>{const c=new Set(l);return c.has(a)?c.delete(a):c.add(a),c})},Ma=async a=>{if(!window.confirm(e("delete.uploadFirst",{name:a})))return;let l=0;try{l=(await rn(a)).count}catch{l=0}if(l>0){if(!window.confirm(e("delete.uploadCascade",{n:l})))return}else if(!window.confirm(e("delete.uploadSecond")))return;U(!0),w(null);try{await Zs(a,{deleteExperiments:l>0}),await Q(),d===a&&m(null)}catch(c){w(String(c))}finally{U(!1)}},Os=async a=>{if(window.confirm(e("delete.experimentFirst"))&&window.confirm(e("delete.experimentSecond"))){U(!0),w(null);try{await tn(a),na===a&&ra(null);const l=await Wt(Ne,Ue,lt);dt(l)}catch(l){w(String(l))}finally{U(!1)}}},at=a=>mt(Math.min(4,Math.max(.5,a))),Fs=Math.max(1,Math.ceil(((Be==null?void 0:Be.total)??0)/lt)),be=i.useMemo(()=>n==="lab_image"?He.filter(a=>a.type==="image"||Qa(a.name)):n==="lab_video"?He.filter(a=>a.type==="video"||ge(a.name)):He,[He,n]),De=Math.max(1,Math.ceil(be.length/qt)),Ps=i.useMemo(()=>{const a=(_e-1)*qt;return be.slice(a,a+qt)},[be,_e]);i.useEffect(()=>{ke(1)},[n]),i.useEffect(()=>{ke(a=>Math.min(a,De))},[De]),i.useEffect(()=>{B&&!be.some(a=>a.name===B)&&oa(null)},[be,B]);const Ca=i.useMemo(()=>Sn(gt,s),[gt,s]),$s=i.useMemo(()=>n==="lab_image"?y.filter(a=>Qa(a.filename)):n==="lab_video"?y.filter(a=>ge(a.filename)):y,[y,n]),v=i.useMemo(()=>rs(ee),[ee]),Ia=i.useMemo(()=>ls(v.prob),[v.prob]);return i.useEffect(()=>{jt(!1)},[k]),i.useEffect(()=>{if(n!=="lab_video")return;let a;const l=()=>{un().then(ys).catch(()=>{})};return l(),a=setInterval(l,1500),()=>{a&&clearInterval(a)}},[n]),i.useEffect(()=>{(n!=="lab_video"||S!=="camera")&&(tt(),ft(!1),ht(null),vt(a=>(a&&URL.revokeObjectURL(a),null))),(n!=="lab_video"||S!=="file")&&et()},[n,S]),i.useEffect(()=>{d||et()},[d]),i.useEffect(()=>()=>{tt(),et()},[]),t.jsxs("div",{className:"flex h-dvh min-h-0 flex-col overflow-hidden bg-surface text-on-surface",children:[t.jsx(Mn,{view:n,setView:r}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex shrink-0 items-center justify-between gap-2 border-b border-outline-variant/15 bg-surface-container-low px-3 py-2 text-xs 2xl:hidden",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{g(!1),u(!0)},children:s==="zh"?"素材":"Assets"}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-3 py-1.5 text-on-surface",onClick:()=>{u(!1),g(!0)},children:s==="zh"?"解算参数":"Solve parameters"})]}),(o||p)&&t.jsx("button",{type:"button",className:"fixed inset-x-0 bottom-0 top-[88px] z-40 bg-black/60 2xl:hidden","aria-label":s==="zh"?"关闭面板":"Close panel",onClick:()=>{u(!1),g(!1)}}),t.jsxs("div",{className:"flex min-h-0 flex-1",children:[t.jsxs("aside",{className:`fixed bottom-0 left-0 top-[88px] z-50 w-64 shrink-0 overflow-y-auto border-r border-outline-variant/15 bg-surface-container-lowest p-3 text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${o?"translate-x-0":"-translate-x-full"}`,children:[t.jsx("div",{className:"mb-2 flex justify-end 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>u(!1),children:s==="zh"?"关闭":"Close"})}),t.jsx("div",{className:"mb-3 font-semibold text-on-surface-variant",children:e("sidebar.assets")}),t.jsxs("label",{className:"mb-3 flex cursor-pointer items-center gap-2 rounded bg-primary-container/30 px-2 py-2 text-on-primary-container",children:[t.jsx(qs,{className:"h-4 w-4"}),t.jsx("span",{children:e("sidebar.upload")}),t.jsx("input",{type:"file",accept:"image/*,video/*",className:"hidden",onChange:async a=>{var c;const l=(c=a.target.files)==null?void 0:c[0];if(l){U(!0),w(null);try{const f=await Bt(l);await Q(),m(f.filename)}catch(f){w(String(f))}finally{U(!1),a.target.value=""}}}})]}),t.jsxs("button",{type:"button",className:"mb-2 flex w-full items-center gap-1 text-left text-on-surface-variant hover:text-on-surface",onClick:()=>{Q().catch(a=>w(String(a))),ke(1),At()},children:[t.jsx(Ws,{className:"h-3 w-3"})," ",e("sidebar.refresh")]}),t.jsx("div",{className:"og-scrollbar max-h-36 overflow-y-auto border-t border-outline-variant/10 pt-2",children:$s.map(a=>t.jsxs("div",{className:`mb-1 flex items-center gap-0.5 rounded px-1 ${d===a.filename?"bg-surface-container":""}`,children:[t.jsxs("button",{type:"button",className:`min-w-0 flex-1 truncate rounded px-1 py-1 text-left ${d===a.filename?"text-primary":""}`,title:a.filename,onClick:()=>{m(a.filename),r(ge(a.filename)?"lab_video":"lab_image")},children:[t.jsx("span",{className:"block truncate",children:rt(a.filename)}),t.jsx("span",{className:"block text-[9px] text-on-surface-variant/90",children:ge(a.filename)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")})]}),t.jsx("button",{type:"button",className:"shrink-0 rounded p-1 text-on-surface-variant hover:bg-surface-container-high hover:text-error",title:e("pool.delete"),"aria-label":e("pool.delete"),onClick:l=>{l.stopPropagation(),Ma(a.filename)},children:t.jsx(ss,{className:"h-3.5 w-3.5"})})]},a.filename))}),t.jsxs("div",{className:"mt-3 border-t border-outline-variant/10 pt-3",children:[t.jsxs("div",{className:"mb-2 flex items-start justify-between gap-2",children:[t.jsx("div",{className:"min-w-0 font-semibold leading-tight text-on-surface-variant",children:e("sidebar.debugCaptures")}),t.jsx("button",{type:"button",className:"shrink-0 rounded bg-primary px-2 py-1 text-[10px] font-medium text-on-primary disabled:opacity-40",disabled:!B||Na,title:B??void 0,onClick:()=>void Ms(),children:B&&/\.avi$/i.test(B)?e("sidebar.importToPoolWithTranscode"):e("sidebar.importToPoolDirect")})]}),Et!=null&&t.jsxs("div",{className:"mb-2 rounded border border-outline-variant/25 bg-surface-container-low px-2 py-2",children:[t.jsxs("div",{className:"flex items-center justify-between gap-2 text-[10px]",children:[t.jsx("span",{className:"text-on-surface-variant",children:vs||e("sidebar.flowPreparing")}),t.jsxs("span",{className:"font-mono text-on-surface",children:[Math.round((Et||0)*100),"%"]})]}),t.jsx("div",{className:"mt-1.5 h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round((Et||0)*100)}%`}})}),Rt&&t.jsx("div",{className:"mt-1 text-[10px] text-on-surface-variant",title:Rt,children:Rt})]}),be.length===0?t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("sidebar.debugEmpty")}):t.jsxs(t.Fragment,{children:[t.jsx("div",{className:"og-scrollbar max-h-[min(22rem,55vh)] space-y-1.5 overflow-y-auto pr-0.5",children:Ps.map(a=>t.jsxs("button",{type:"button",className:`flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left transition-colors ${B===a.name?"border-primary bg-primary-container/20":"border-outline-variant/25 hover:bg-surface-container"}`,onClick:()=>oa(a.name),children:[a.type==="image"?t.jsx("img",{src:sn(a.name),alt:"",className:"h-11 w-11 shrink-0 rounded object-cover"}):t.jsx("div",{className:"flex h-11 w-11 shrink-0 items-center justify-center rounded bg-surface-container-high text-[9px] text-on-surface-variant",children:"video"}),t.jsxs("div",{className:"min-w-0 flex-1",children:[t.jsx("div",{className:"truncate font-mono text-[10px] text-on-surface",title:a.name,children:rt(a.name)}),t.jsxs("div",{className:"text-[9px] text-on-surface-variant",children:[a.type==="video"||ge(a.name)?e("sidebar.assetTypeVideo"):e("sidebar.assetTypeImage")," ","· ",$e(a.modified,s)," · ",Zt(a.size)]})]})]},a.name))}),t.jsxs("div",{className:"mt-2 flex items-center justify-between gap-1 text-[10px] text-on-surface-variant",children:[t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e<=1,onClick:()=>ke(a=>Math.max(1,a-1)),children:e("history.prev")}),t.jsx("span",{className:"tabular-nums",children:e("sidebar.debugPage",{cur:_e,total:De})}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-0.5 disabled:opacity-40",disabled:_e>=De,onClick:()=>ke(a=>Math.min(De,a+1)),children:e("history.next")})]})]})]})]}),(n==="lab_image"||n==="lab_video")&&t.jsxs("div",{className:"flex min-h-0 min-w-0 flex-1",children:[t.jsxs("main",{className:"og-scrollbar min-h-0 min-w-0 flex-1 overflow-y-auto p-4",children:[n==="lab_video"&&t.jsxs("div",{className:"mb-3 max-w-5xl rounded-lg border border-outline-variant/25 bg-surface-container-low/80 p-3 text-[11px] leading-relaxed text-on-surface",children:[t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.videoLiveIntro")}),t.jsx("p",{className:"mt-1.5 text-[10px] leading-snug text-on-surface-variant/95",children:e("lab.videoMjpegHint")}),t.jsxs("div",{className:"mt-2 flex flex-wrap gap-2",children:[t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="file"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("file"),children:e("lab.previewModeFile")}),t.jsx("button",{type:"button",className:`rounded px-3 py-1.5 text-[11px] font-medium ${S==="camera"?"bg-primary text-on-primary-container":"border border-outline-variant/40 bg-surface-container text-on-surface"}`,onClick:()=>Se("camera"),children:e("lab.previewModeCamera")}),t.jsx("button",{type:"button",className:"rounded border border-outline-variant/40 bg-surface-container px-3 py-1.5 text-[11px] font-medium text-on-surface",onClick:()=>void Is(),children:e("lab.stopCameraPreview")})]})]}),t.jsxs("div",{className:"relative aspect-video w-full max-w-5xl overflow-hidden rounded-lg border border-outline-variant/20 bg-surface-container-lowest",children:[Ia!=null&&t.jsxs("div",{className:"pointer-events-none absolute right-2 top-2 z-20 rounded border border-white/30 bg-black/55 px-2 py-1 text-[10px] font-semibold tabular-nums text-white",children:[e("lab.previewConfidence"),": ",Ia.toFixed(1),"%"]}),n==="lab_video"&&S==="camera"?t.jsx("div",{className:"relative flex h-full min-h-[220px] flex-col items-center justify-center gap-3 bg-black p-2",children:t.jsxs("div",{className:"relative inline-block max-h-[70vh] max-w-full",children:[K&&bt?t.jsx("img",{ref:Le,src:bt,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight})}):K&&!bt?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):xt==="busy"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.videoMjpegHint")})}):xt==="fail"?t.jsx("div",{className:"flex min-h-[200px] max-w-md flex-col items-center justify-center gap-2 px-4 text-center text-[11px] text-on-surface-variant",children:t.jsx("span",{className:"text-error",children:e("lab.cameraStreamStatusFail")})}):xt==="unknown"?t.jsxs("div",{className:"flex min-h-[200px] w-full min-w-[280px] flex-col items-center justify-center gap-2 text-[11px] text-on-surface-variant",children:[t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"}),t.jsx("span",{children:e("lab.cameraPreviewLoading")})]}):t.jsx("img",{ref:Le,src:`/api/dev/debug/camera/stream?t=${xa}`,alt:"",className:"max-h-[70vh] w-full min-h-[120px] object-contain",onLoad:a=>ma({w:a.currentTarget.naturalWidth,h:a.currentTarget.naturalHeight}),onError:()=>{(async()=>{const a=await as();if(a==="busy"){G("busy");return}if(a==="fail"){G("fail");return}if(It.current>=6){G("fail");return}It.current+=1,window.setTimeout(()=>We(Date.now()),400)})()}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}):d?t.jsx("div",{className:"og-scrollbar relative h-full min-h-[200px] overflow-auto",children:n==="lab_video"?t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(za,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(Ha,{className:"inline h-3 w-3"})})]}),t.jsxs("div",{className:"flex min-h-[200px] flex-col items-center justify-center gap-2 bg-black py-2",children:[t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("video",{ref:Ie,src:Ye,loop:!0,playsInline:!0,autoPlay:!0,muted:!0,preload:"metadata",className:"max-h-[70vh] w-full max-w-full object-contain",onError:()=>pt(e("lab.videoPreviewFailed")),onLoadedData:()=>{pt(null);const a=Ie.current;a&&a.play().catch(()=>{})}}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})}),Ee&&t.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-1.5 text-[11px] text-error",children:Ee}),Ra&&t.jsxs("div",{className:"max-w-3xl rounded border border-primary/40 bg-primary/10 px-3 py-2 text-[11px] text-on-surface",children:[t.jsx("div",{className:"font-semibold text-primary",children:e("lab.transcode.title")}),t.jsx("div",{className:"mt-1 text-on-surface-variant",children:e("lab.transcode.desc")}),ja!=null&&t.jsxs("div",{className:"mt-2",children:[t.jsx("div",{className:"mb-1 text-[10px] text-on-surface-variant",children:bs||e("lab.transcode.running")}),t.jsx("div",{className:"h-1.5 w-full overflow-hidden rounded bg-surface-container-highest",children:t.jsx("div",{className:"h-full bg-primary transition-all",style:{width:`${Math.round(ja*100)}%`}})})]}),t.jsx("div",{className:"mt-2",children:t.jsx("button",{type:"button",className:"rounded bg-primary px-3 py-1.5 text-[11px] font-medium text-on-primary disabled:opacity-50",disabled:ya,onClick:()=>void Es(),children:e("lab.transcode.button")})})]})]})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"absolute left-2 top-2 z-20 flex flex-wrap gap-1",children:[t.jsxs("button",{type:"button",title:e("lab.grid"),className:`rounded border px-2 py-1 text-[10px] ${ze?"border-primary bg-primary/20":"border-white/30 bg-black/40"} text-white`,onClick:()=>ca(a=>!a),children:[t.jsx(za,{className:"mr-1 inline h-3 w-3"}),e("lab.grid")]}),t.jsx("button",{type:"button",title:e("lab.zoomOut"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe-.25),children:t.jsx(Va,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomIn"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>at(xe+.25),children:t.jsx(Wa,{className:"inline h-3 w-3"})}),t.jsx("button",{type:"button",title:e("lab.zoomReset"),className:"rounded border border-white/30 bg-black/40 px-2 py-1 text-[10px] text-white",onClick:()=>mt(1),children:t.jsx(Ha,{className:"inline h-3 w-3"})})]}),t.jsx("div",{className:"inline-block origin-top-left transition-transform",style:{transform:`scale(${xe})`},children:t.jsxs("div",{className:"relative inline-block",children:[ze&&t.jsx("div",{className:"pointer-events-none absolute inset-0 z-[1]",style:{backgroundImage:["linear-gradient(to right, rgba(255,255,255,0.12) 1px, transparent 1px)","linear-gradient(to bottom, rgba(255,255,255,0.12) 1px, transparent 1px)"].join(","),backgroundSize:"48px 48px"}}),t.jsx("img",{ref:Ct,src:Ye,alt:"preview",className:"max-h-[70vh] w-full object-contain"}),t.jsx("canvas",{ref:he,className:"pointer-events-none absolute left-0 top-0"})]})})]})}):t.jsx("div",{className:"flex h-full items-center justify-center px-4 text-center text-on-surface-variant",children:e(n==="lab_video"?"lab.selectOrUploadVideo":"lab.selectOrUpload")}),ne&&t.jsx("div",{className:"absolute inset-0 flex items-center justify-center bg-black/40",children:t.jsx(st,{className:"h-8 w-8 animate-spin text-primary"})})]}),(d&&n==="lab_image"||n==="lab_video"&&(S==="file"&&d||S==="camera"))&&t.jsxs("div",{className:"mt-2 flex flex-wrap items-center gap-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 px-3 py-2 text-xs text-on-surface",children:[t.jsx("span",{className:"shrink-0 font-medium text-on-surface-variant",children:e("lab.layers")}),t.jsx("div",{className:"flex flex-wrap gap-x-4 gap-y-1",children:["matched","pattern","all","rejected"].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-1",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:V[a],onChange:l=>ds(c=>({...c,[a]:l.target.checked}))}),t.jsx("span",{children:e(a==="matched"?"lab.layer.matched":a==="pattern"?"lab.layer.pattern":a==="all"?"lab.layer.all":"lab.layer.rejected")})]},a))})]}),(d||n==="lab_video"&&S==="camera")&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 grid gap-2 sm:grid-cols-2",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.solveSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:ee?t.jsxs("div",{className:"space-y-1 text-on-surface",children:[v.tSolveMs!=null&&t.jsxs("div",{className:"space-y-0.5",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveComputeMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[v.tSolveMs.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveComputeHelp")})]}),Nt!=null&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.solveRoundTripMs")}),t.jsxs("span",{className:"font-mono tabular-nums",children:[Nt.toFixed(0)," ms"]})]}),t.jsx("p",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.solveRoundTripHelp")})]}),(v.tBackendTotalMs!=null||v.tOpenDecodeMs!=null||v.tPreprocessMs!=null||v.tExtractMs!=null||v.tSolveMs!=null)&&t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/10 pt-1",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.backendTotalMs")}),t.jsx("span",{className:"font-mono tabular-nums",children:v.tBackendTotalMs!=null?`${v.tBackendTotalMs.toFixed(0)} ms`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[8px] text-on-surface-variant/90",children:[t.jsxs("span",{children:[e("lab.metric.openDecodeMs"),":"," ",v.tOpenDecodeMs!=null?`${v.tOpenDecodeMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.preprocessMs"),":"," ",v.tPreprocessMs!=null?`${v.tPreprocessMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.extractMs"),":"," ",v.tExtractMs!=null?`${v.tExtractMs.toFixed(0)} ms`:e("common.placeholder")]}),t.jsxs("span",{children:[e("lab.metric.solveOnlyMs"),":"," ",v.tSolveMs!=null?`${v.tSolveMs.toFixed(0)} ms`:e("common.placeholder")]})]})]}),(v.raDeg!=null||v.decDeg!=null)&&t.jsxs("div",{className:"leading-tight",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.radec")}),t.jsxs("div",{className:"mt-0.5 font-mono text-[9px]",children:["α ",it(v.raDeg)," · δ"," ",it(v.decDeg)]})]}),t.jsxs("div",{className:"flex flex-wrap gap-x-2 gap-y-0.5 text-[9px]",children:[v.matches!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.matches")})," ",t.jsx("span",{className:"font-mono",children:v.matches})]}),v.rmseArcsec!=null&&t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.rmse")})," ",t.jsxs("span",{className:"font-mono",children:[v.rmseArcsec.toFixed(2),"″"]})]}),v.prob!=null&&t.jsxs("span",{className:"inline-flex max-w-full flex-col gap-0.5",children:[t.jsxs("span",{children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.metric.prob")})," ",t.jsx("span",{className:"font-mono",children:ye(v.prob,ee??void 0).line})]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probHelp")}),ye(v.prob,ee??void 0).rawLine&&t.jsxs(t.Fragment,{children:[t.jsxs("span",{className:"text-[8px] text-on-surface-variant",children:[e("lab.metric.probRaw"),":"," ",ye(v.prob,ee??void 0).rawLine]}),t.jsx("span",{className:"text-[8px] leading-snug text-on-surface-variant/90",children:e("lab.metric.probRawHelp")})]})]})]}),v.status&&t.jsxs("div",{className:"border-t border-outline-variant/15 pt-1 text-[9px]",children:[t.jsxs("span",{className:"text-on-surface-variant",children:[e("lab.metric.status")," "]}),t.jsx("span",{className:"font-mono text-secondary",children:v.status})]})]}):t.jsx("p",{className:"text-on-surface-variant",children:e("common.placeholder")})})]}),t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-xs font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.imageSection"),t.jsx(Fe,{className:"h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsxs("div",{className:"space-y-0.5 border-t border-outline-variant/15 px-3 py-2 text-[10px]",children:[t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.resolution")}),t.jsx("span",{className:"font-mono tabular-nums",children:Ut.w>0?`${Ut.w}×${Ut.h}`:e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.starsDetected")}),t.jsx("span",{className:"font-mono tabular-nums",children:Sa??e("common.placeholder")})]}),t.jsxs("div",{className:"flex justify-between gap-2",children:[t.jsx("span",{className:"text-on-surface-variant",children:e("lab.fwhm")}),t.jsx("span",{children:e("common.placeholder")})]})]})]})]}),d&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"mt-2 flex flex-wrap gap-4 text-xs text-on-surface-variant",children:[t.jsxs("span",{children:[e("lab.file"),":"," ",t.jsx("span",{className:"font-mono text-on-surface",title:d,children:rt(d,22,18)})]}),((La=y.find(a=>a.filename===d))==null?void 0:La.source)&&t.jsxs("span",{className:"rounded bg-surface-container-high px-2 py-0.5",children:[e("lab.source"),":"," ",(Da=y.find(a=>a.filename===d))==null?void 0:Da.source]})]}),t.jsxs("details",{open:!0,className:"mt-2 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90 text-xs shadow-sm",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center gap-2 px-3 py-2 font-semibold text-on-surface [&::-webkit-details-marker]:hidden",children:[e("lab.meta.title"),yt&&t.jsx(st,{className:"h-3.5 w-3.5 animate-spin"}),t.jsx(Fe,{className:"ml-auto h-4 w-4 shrink-0 text-on-surface-variant"})]}),t.jsx("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:Ca.length>0?t.jsx("dl",{className:"grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-3",children:Ca.map(a=>t.jsxs("div",{children:[t.jsx("dt",{className:"text-[10px] text-on-surface-variant",children:e(a.key)}),t.jsx("dd",{className:"text-[11px] font-medium text-on-surface",children:a.value})]},`${a.key}-${a.value}`))}):gt&&!yt?t.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:e("lab.meta.partial")}):yt?null:t.jsx("p",{className:"text-[10px] text-on-surface-variant",children:e("lab.meta.noSidecar")})})]})]})]}),(d||n==="lab_video"&&S==="camera")&&(Ae.length>0||k||T)&&t.jsxs(t.Fragment,{children:[Ae.length>0&&t.jsxs("section",{className:"mt-3 rounded-lg border border-outline-variant/25 bg-surface-container-lowest/90",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.solveHistoryTitle")}),t.jsxs("span",{className:"ml-2 normal-case text-on-surface-variant/70",children:["(",Ae.length,"/",es,")"]})]}),t.jsx("div",{className:"og-scrollbar max-h-[min(36vh,18rem)] space-y-2 overflow-y-auto p-3",children:Ae.map(a=>{var $;const l=a.batch,c=a.single,f=$e(new Date(a.at).toISOString(),s),j=l&&l.results.length?e("results.historyBatch",{count:l.results.length}):e("results.historySingle");return t.jsxs("details",{className:"rounded border border-outline-variant/20 bg-surface-container/80 text-xs",children:[t.jsx("summary",{className:"cursor-pointer list-none px-3 py-2 font-medium text-on-surface [&::-webkit-details-marker]:hidden",children:t.jsxs("span",{children:[f," · ",j]})}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[($=l==null?void 0:l.results)!=null&&$.length?t.jsx("div",{className:"og-scrollbar flex gap-2 overflow-x-auto pb-1",children:l.results.map((E,D)=>{const C=E.result,ce=E.success===!0;return t.jsxs("div",{className:"min-w-[12rem] max-w-xs shrink-0 rounded border border-outline-variant/20 bg-surface-container-lowest p-2",children:[t.jsx("div",{className:"border-b border-outline-variant/10 pb-1 text-[10px] font-semibold text-secondary",children:String(E.label??D)}),ce&&C?t.jsx(nt,{result:C,t:e,roundTripMs:null}):t.jsx("div",{className:"mt-1 text-[10px] text-error",children:String(E.error??"—")})]},D)})}):null,c&&c.result?t.jsx("div",{className:"mt-2 rounded border border-outline-variant/15 p-2",children:t.jsx(nt,{result:c.result,t:e,roundTripMs:null})}):null,t.jsxs("details",{className:"mt-2",children:[t.jsx("summary",{className:"cursor-pointer text-[10px] text-primary hover:underline",children:e("results.viewRaw")}),t.jsx("pre",{className:"og-scrollbar mt-1 max-h-32 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify({single:c,batch:l},null,2)})]})]})]},a.id)})})]}),(k||T)&&t.jsxs("section",{className:"mt-3 max-h-[min(50vh,28rem)] rounded-lg border border-outline-variant/25 bg-surface-container-lowest/95",children:[t.jsxs("div",{className:"flex h-9 shrink-0 items-center justify-between border-b border-outline-variant/15 px-3 text-[10px] uppercase text-on-surface-variant",children:[t.jsx("span",{children:e("results.title")}),t.jsxs("div",{className:"flex gap-3",children:[(Oa=T==null?void 0:T.results)!=null&&Oa.length?t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!(!d||!T)){for(const a of T.results){if(!a.success)continue;const l=a.result;await zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(l??null),replay:{layers:V,params:b}})}w(null)}},children:e("results.saveBatchAll")}):null,k&&t.jsx("button",{type:"button",className:"rounded bg-surface-container px-2 py-0.5 normal-case",onClick:async()=>{if(!d&&hs!=="camera")return;const a=k.result;await zt({input_name:d??e("lab.cameraSnapshotName"),preset_label:"manual",result_json:k,metrics:Kt(a??null),replay:{layers:V,params:b}}),w(null)},children:e("results.saveCurrent")})]})]}),t.jsx("div",{className:"og-scrollbar min-h-0 overflow-y-auto p-3",children:t.jsxs("div",{className:"og-scrollbar flex gap-3 overflow-x-auto text-xs",children:[T==null?void 0:T.results.map((a,l)=>{const c=a.result,f=fs[l]??!1;return t.jsxs("div",{className:"min-w-[15rem] max-w-sm shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 font-semibold text-secondary",children:String(a.label)}),a.success?t.jsxs(t.Fragment,{children:[t.jsx(nt,{result:c,t:e,roundTripMs:null}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>wt(j=>({...j,[l]:!f})),children:e(f?"results.hideRaw":"results.viewRaw")}),f&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-40 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(a,null,2)})]}):t.jsx("div",{className:"mt-2 text-[10px] text-error",children:String(a.error)}),a.success===!0&&d?t.jsx("button",{type:"button",className:"mt-3 w-full rounded bg-surface-container-high py-1.5 text-[10px] font-medium",onClick:()=>zt({input_name:d,preset_label:String(a.label),result_json:a,metrics:Kt(c??null),replay:{layers:V,params:b}}).catch(j=>w(String(j))),children:e("results.saveRow")}):null]},l)}),k&&!T&&t.jsxs("div",{className:"min-w-[16rem] max-w-md shrink-0 rounded-lg border border-outline-variant/25 bg-surface-container p-3 shadow-sm",children:[t.jsx("div",{className:"border-b border-outline-variant/15 pb-2 text-[10px] font-semibold uppercase text-on-surface-variant",children:e("results.title")}),t.jsx(nt,{result:k.result,t:e,roundTripMs:Nt}),t.jsx("button",{type:"button",className:"mt-2 text-[10px] text-primary hover:underline",onClick:()=>jt(a=>!a),children:e(va?"results.hideRaw":"results.viewRaw")}),va&&t.jsx("pre",{className:"og-scrollbar mt-1 max-h-48 overflow-auto rounded bg-surface-container-highest p-2 text-[9px] text-on-surface-variant",children:JSON.stringify(k,null,2)})]})]})})]})]})]}),t.jsxs("aside",{className:`fixed bottom-0 right-0 top-[88px] z-50 flex min-h-0 w-80 shrink-0 flex-col border-l border-outline-variant/15 bg-surface-container-low text-xs shadow-2xl transition-transform 2xl:static 2xl:translate-x-0 2xl:shadow-none ${p?"translate-x-0":"translate-x-full"}`,children:[t.jsx("div",{className:"flex shrink-0 justify-end border-b border-outline-variant/15 p-2 2xl:hidden",children:t.jsx("button",{type:"button",className:"rounded border border-outline-variant/30 px-2 py-1",onClick:()=>g(!1),children:s==="zh"?"关闭":"Close"})}),n!=="lab_video"&&t.jsxs("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container",onClick:js,disabled:ne,children:e("btn.solveOne")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary",onClick:Ns,disabled:ne,children:e("btn.solveBatch")})]}),n==="lab_video"&&t.jsx("div",{className:"shrink-0 space-y-2 border-b border-outline-variant/20 p-4 pb-3",children:S==="file"?t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Ss(),disabled:ne||!Ea||fe,children:e("lab.solveFileStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>et(),disabled:!fe,children:e("lab.solveFileStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void ks(),disabled:ne||!Ea||fe,children:e("lab.solveCurrentFrame")})]}):t.jsxs(t.Fragment,{children:[t.jsx("button",{type:"button",className:"w-full rounded bg-primary py-2.5 font-semibold text-on-primary-container disabled:opacity-40",onClick:()=>Cs(),disabled:ne||Re||K,children:e("lab.solveCameraStart")}),t.jsx("button",{type:"button",className:"w-full rounded bg-error/80 py-2.5 font-semibold text-on-error disabled:opacity-40",onClick:()=>tt(),disabled:!Re,children:e("lab.solveCameraStop")}),t.jsx("button",{type:"button",className:"w-full rounded bg-secondary/80 py-2.5 font-semibold text-on-secondary disabled:opacity-40",onClick:()=>void _s(),disabled:ne||K,children:e("lab.solveCameraFrame")}),t.jsxs("label",{className:"flex items-center gap-2 rounded border border-outline-variant/25 px-2 py-1.5 text-[11px]",children:[t.jsx("input",{type:"checkbox",checked:ha,onChange:a=>xs(a.target.checked)}),t.jsx("span",{children:"自动保持(解算成功后冻结) / Auto Hold"})]}),K&&t.jsx("button",{type:"button",className:"w-full rounded bg-tertiary/80 py-2.5 font-semibold text-on-tertiary disabled:opacity-40",onClick:()=>Ls(),children:"继续实时 / Resume Live"}),K&&t.jsxs("div",{className:"text-[10px] text-on-surface-variant",children:["已冻结帧 / Frozen frame ",ps??"-"]})]})}),n==="lab_video"&&Ge&&t.jsxs("div",{className:"shrink-0 border-b border-outline-variant/20 px-4 py-2 text-[10px] text-on-surface-variant",children:[t.jsx("div",{className:"font-medium text-on-surface",children:e("lab.systemLoad")}),t.jsxs("div",{children:["CPU ",Ge.cpu_usage,"% · RAM ",Ge.memory_usage,"% ·"," ",Ge.temperature,"°C"]})]}),t.jsxs("div",{className:"og-scrollbar min-h-0 flex-1 overflow-y-auto p-4",children:[t.jsxs("details",{open:!0,className:"rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.title"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockSolveIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsxs("label",{className:"block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.solveProfile")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.solveProfileHelp")}),t.jsxs("select",{className:"w-full rounded bg-surface-container-highest px-2 py-1",value:b.solve_profile??"balanced",onChange:a=>x(l=>({...l,solve_profile:a.target.value})),children:[t.jsx("option",{value:"speed",children:e("params.solveProfileSpeed")}),t.jsx("option",{value:"balanced",children:e("params.solveProfileBalanced")}),t.jsx("option",{value:"robust",children:e("params.solveProfileRobust")})]})]}),t.jsx(z,{label:e("params.fov"),helpKey:"params.fovHelp",value:b.fov_estimate??"",onChange:a=>x(l=>({...l,fov_estimate:a}))}),t.jsx(z,{label:e("params.fovErr"),helpKey:"params.fovErrHelp",value:b.fov_max_error??"",onChange:a=>x(l=>({...l,fov_max_error:a}))}),t.jsx(z,{label:e("params.timeout"),helpKey:"params.timeoutHelp",value:b.solve_timeout_ms??"",onChange:a=>x(l=>({...l,solve_timeout_ms:a}))}),t.jsx(z,{label:e("params.solveIntervalMs"),helpKey:"params.solveIntervalMsHelp",value:kt??"",onChange:a=>ga(a==null?Tt:Math.round(a))}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalBound",{min:Pt,max:$t,effective:oe})}),t.jsx("p",{className:"text-[9px] leading-snug text-on-surface-variant/80",children:e("params.solveIntervalIndependent")}),n==="lab_video"&&t.jsxs("label",{className:"mt-2 block",children:[t.jsx("span",{className:"text-[10px] font-medium text-on-surface-variant",children:e("params.centroidRejectionLevel")}),t.jsx("p",{className:"mb-1 mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.centroidRejectionLevelHelp")}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"1"}),t.jsx("input",{type:"range",min:1,max:5,step:1,className:"h-2 flex-1 accent-primary",value:b.centroid_rejection_level??3,onChange:a=>x(l=>({...l,centroid_rejection_level:Number(a.target.value)}))}),t.jsx("span",{className:"w-5 shrink-0 text-center text-[9px] text-on-surface-variant",children:"5"}),t.jsx("span",{className:"ml-1 min-w-[1.25rem] tabular-nums text-[10px] font-semibold text-on-surface",children:b.centroid_rejection_level??3})]}),t.jsx("p",{className:"mt-0.5 text-[9px] text-on-surface-variant/90",children:e("params.centroidRejectionScale")})]}),t.jsx(z,{label:e("params.ra"),helpKey:"params.raHelp",value:b.hint_ra_deg??"",onChange:a=>x(l=>({...l,hint_ra_deg:a}))}),t.jsx(z,{label:e("params.dec"),helpKey:"params.decHelp",value:b.hint_dec_deg??"",onChange:a=>x(l=>({...l,hint_dec_deg:a}))}),t.jsx(z,{label:e("params.maxSide"),helpKey:"params.maxSideHelp",value:b.max_image_side??"",onChange:a=>x(l=>({...l,max_image_side:a}))}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary",checked:b.detail_level==="full",onChange:a=>x(l=>({...l,detail_level:a.target.checked?"full":"summary"}))}),t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.detailLevelFull")})]}),t.jsxs("label",{className:"mt-1.5 flex cursor-pointer items-start gap-2",children:[t.jsx("input",{type:"checkbox",className:"accent-primary mt-0.5",checked:!!b.large_scale_bg_subtract,onChange:a=>x(l=>({...l,large_scale_bg_subtract:a.target.checked}))}),t.jsxs("span",{children:[t.jsx("span",{className:"text-[10px] text-on-surface-variant",children:e("params.largeScaleBg")}),t.jsx("p",{className:"mt-0.5 text-[9px] leading-snug text-on-surface-variant/85",children:e("params.largeScaleBgHelp")})]})]})]})]})]}),t.jsxs("details",{open:!0,className:"mt-3 rounded-lg border border-outline-variant/20 bg-surface-container-highest/30",children:[t.jsxs("summary",{className:"flex cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 font-semibold text-on-surface-variant [&::-webkit-details-marker]:hidden",children:[e("params.centroid"),t.jsx(Fe,{className:"h-4 w-4 shrink-0"})]}),t.jsxs("div",{className:"border-t border-outline-variant/15 p-3 pt-2",children:[t.jsx("p",{className:"mb-3 text-[10px] leading-relaxed text-on-surface-variant/90",children:e("params.blockCentroidIntro")}),t.jsxs("div",{className:"space-y-2",children:[t.jsx(z,{label:e("params.sigma"),helpKey:"params.sigmaHelp",step:.1,value:((Fa=b.centroid)==null?void 0:Fa.sigma)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,sigma:a}}))}),t.jsx(z,{label:e("params.maxArea"),helpKey:"params.maxAreaHelp",value:((Pa=b.centroid)==null?void 0:Pa.max_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,max_area:a}}))}),t.jsx(z,{label:e("params.minArea"),helpKey:"params.minAreaHelp",value:(($a=b.centroid)==null?void 0:$a.min_area)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,min_area:a}}))}),t.jsx(z,{label:e("params.filtsize"),helpKey:"params.filtsizeHelp",step:2,value:((Ta=b.centroid)==null?void 0:Ta.filtsize)??"",onChange:a=>x(l=>({...l,centroid:{...l.centroid,filtsize:a}}))})]})]})]}),t.jsxs("div",{className:"mt-4 rounded-md border border-outline-variant/20 bg-surface-container-highest/40 p-3",children:[t.jsx("div",{className:"mb-2 font-semibold text-on-surface-variant",children:e("sidebar.batchPresets")}),t.jsx("p",{className:"mb-2 text-[10px] leading-snug text-on-surface-variant",children:e("sidebar.batchHint")}),t.jsx("div",{className:"og-scrollbar max-h-36 space-y-1.5 overflow-y-auto",children:[...I,...se].map(a=>t.jsxs("label",{className:"flex cursor-pointer items-center gap-2",children:[t.jsx("input",{type:"checkbox",checked:Xt.has(a.id),onChange:()=>Ds(a.id)}),t.jsx("span",{className:"truncate",children:a.name})]},a.id))})]}),t.jsxs("div",{className:"mt-6 border-t border-outline-variant/20 pt-4",children:[t.jsx("div",{className:"mb-2 font-semibold",children:e("btn.applyPresets")}),t.jsx("div",{className:"og-scrollbar max-h-24 space-y-1 overflow-y-auto",children:[...I,...se].map(a=>t.jsx("button",{type:"button",className:"block w-full truncate text-left text-primary hover:underline",onClick:()=>ws(a.params),children:a.name},a.id))}),t.jsxs("div",{className:"mt-3 flex gap-1",children:[t.jsx("input",{className:"flex-1 rounded bg-surface-container-highest px-2 py-1",placeholder:e("placeholder.newPreset"),value:ut,onChange:a=>la(a.target.value)}),t.jsx("button",{type:"button",className:"rounded bg-surface-container-high px-2",onClick:async()=>{if(ut.trim()){U(!0);try{await en(ut.trim(),b),la(""),await Q();const a=await Ht("user");Yt(a.presets)}catch(a){w(String(a))}finally{U(!1)}}},children:e("btn.savePreset")})]})]})]})]})]}),n==="pool"&&t.jsx(In,{uploads:y,onDelete:a=>void Ma(a)}),n==="history"&&t.jsx(Cn,{historyQ:Ne,setHistoryQ:cs,historyPage:Ue,setHistoryPage:sa,historyData:Be,historyExpandId:na,setHistoryExpandId:ra,historyTotalPages:Fs,onDeleteExperiment:a=>void Os(a),onSearch:()=>Wt(Ne,1,lt).then(a=>{sa(1),dt(a)})})]}),t.jsx("div",{className:"pointer-events-none fixed bottom-4 right-4 z-[100] flex max-w-sm flex-col gap-2",children:os.map(a=>t.jsx("div",{className:`pointer-events-auto rounded border px-3 py-2 text-left text-xs shadow-lg ${a.variant==="error"?"border-error/50 bg-error-container/95 text-on-error-container":a.variant==="warn"?"border-amber-500/40 bg-surface-container-highest/95 text-on-surface":"border-outline-variant/40 bg-surface-container-high/95 text-on-surface"}`,children:a.text},a.id))})]})}Bs.createRoot(document.getElementById("root")).render(t.jsx(Hs.StrictMode,{children:t.jsx(zs,{children:t.jsx(Dn,{})})})); diff --git a/web/static/analysis-lab/assets/camera-aU1xxh-S.js b/web/static/analysis-lab/assets/camera-aU1xxh-S.js deleted file mode 100644 index f557e53..0000000 --- a/web/static/analysis-lab/assets/camera-aU1xxh-S.js +++ /dev/null @@ -1,56 +0,0 @@ -import{r as s,j as e,a as Ft,R as $t}from"./client-D1ZVDB-N.js";import{c as _,u as Bt,T as Pt,I as Tt}from"./index-CV2LaaT8.js";import{u as It,S as Ot,C as Ua,X as Dt,r as At,b as Ht}from"./http-lbh6zBy9.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 La=_("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** - * @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 zt=_("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** - * @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 Gt=_("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** - * @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 qt=_("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** - * @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 Ut=_("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** - * @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 Lt=_("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** - * @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 Wa=_("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** - * @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 Wt=_("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** - * @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 Vt=_("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** - * @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 Jt=_("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** - * @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 Kt=_("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Zt="/api/dev/debug";function L(t){return`${Zt}${t}`}async function d(t,g={}){const v=t.startsWith("/api/debug/")?t.replace("/api/debug/","/api/dev/debug/"):t;return At(v,g)}const Xt=["640x360","1280x720","1600x900","1920x1020"],Qt=[0,90,180,270],ke=12;function w(t,g,v){return Math.min(v,Math.max(g,t))}function j(t,g){return t==null||Number.isNaN(Number(t))?g:Number(t)}function Va(t){if(!t)return"0 B";const g=["B","KB","MB","GB"];let v=0,h=t;for(;h>=1024&&v0&&h>=v?"busy":"ok"}catch{return"fail"}}function $({label:t,value:g,min:v,max:h,step:n,onChange:_e,disabled:u=!1,unit:W=""}){return e.jsxs("label",{className:`block ${u?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:t}),e.jsxs("span",{className:"font-mono text-[11px]",children:[g.toFixed(n>=1?0:n>=.1?1:2),W]})]}),e.jsx("input",{type:"range",min:v,max:h,step:n,value:g,disabled:u,onChange:te=>_e(Number(te.target.value)),className:"w-full accent-primary"})]})}function Yt(){var Ra,Ea,Ma,Fa,$a,Ba,Pa,Ta,Ia,Oa,Da,Aa,Ha;const{t,locale:g,setLocale:v}=Bt(),{info:h}=It(),[n,_e]=s.useState(null),[u,W]=s.useState(!1),[te,Ce]=s.useState(()=>Date.now()),[Re,i]=s.useState(null),[re,H]=s.useState(null),[We,z]=s.useState(!1),[V,p]=s.useState(null),[M,ne]=s.useState(!1),[Ee,Ve]=s.useState(!1),[Je,Ke]=s.useState(!1),[Ze,Xe]=s.useState("8"),[Qe,Ye]=s.useState("8"),[ea,aa]=s.useState("1280x720"),[ta,ra]=s.useState("supersample"),[J,G]=s.useState(!1),[na,Ka]=s.useState(!0),[Me,Za]=s.useState(!0),[Fe,Xa]=s.useState(!1),[sa,Qa]=s.useState(!1),[oa,Ya]=s.useState(!1),[$e,et]=s.useState({mean:0,std:0,over:0}),[at,se]=s.useState(0),[o,Be]=s.useState(null),[ia,ca]=s.useState(0),[la,da]=s.useState(180),[oe,ua]=s.useState(!1),[ie,ma]=s.useState(!1),[c,y]=s.useState({exposure:5e3,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxUs:2e6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[ce,b]=s.useState(!1),[Pe,pa]=s.useState(""),[xa,ha]=s.useState(""),[fa,tt]=s.useState([]),[Te,B]=s.useState(!1),[K,rt]=s.useState([]),[ba,ga]=s.useState(!1),[N,Ie]=s.useState(null),[nt,Oe]=s.useState(!1),[le,De]=s.useState(null),[Ae,de]=s.useState(1),C=s.useRef(null),ue=s.useRef(null),Z=s.useRef(null),me=s.useRef(null),pe=s.useRef(null),P=s.useRef(null),xe=s.useRef(null),D=s.useRef(!1),he=s.useRef(null),T=s.useRef([]),q=s.useRef(0),He=s.useRef(null),va=()=>{T.current=[],q.current=0,he.current=null,se(0)},S=async()=>{try{const a=await d("/api/debug/camera/status",{cache:"no-store"});_e(a),a.streaming||(W(!1),D.current=!1)}catch(a){i(a instanceof Error?a.message:String(a))}},X=()=>{xe.current!=null&&(window.clearTimeout(xe.current),xe.current=null)},ya=async()=>{if(!M){ne(!0),i(null),H(null),z(!1);try{X(),n!=null&&n.streaming||await d("/api/debug/camera/start",{method:"POST"});const a=Date.now(),r=await Ja();if(r==="busy"){H(t("cam.err.streamBusy")),z(!0);return}if(r==="fail"){H(t("cam.err.streamProbeFailed")),z(!1);return}W(!0),D.current=!0,p(t("cam.notice.previewStart")),va(),he.current=performance.now(),Ce(a),await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},st=async()=>{if(!M){ne(!0),i(null);try{X(),W(!1),D.current=!1,H(null),z(!1),va(),C.current&&(C.current.onload=null,C.current.onerror=null,C.current.src="",C.current.removeAttribute("src")),Ce(Date.now()),p(t("cam.notice.previewStop"))}catch(a){i(a instanceof Error?a.message:String(a))}finally{ne(!1)}}},ot=async()=>{if(!(!u&&!(n!=null&&n.streaming))){Ke(!0),i(null);try{const a=await d("/api/debug/camera/capture",{method:"POST"});p(t("cam.notice.captureSaved",{name:a.filename||"capture"}))}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ke(!1)}}},it=async()=>{if(!Ee){Ve(!0),i(null);try{if(n!=null&&n.recording)await d("/api/debug/camera/record/stop",{method:"POST"}),p(t("cam.notice.recordStop"));else{const a=await d("/api/debug/camera/record/start",{method:"POST"});p(t("cam.notice.recordStart",{name:a.filename||"video.avi"}))}await S()}catch(a){i(a instanceof Error?a.message:String(a))}finally{Ve(!1)}}},ct=async()=>{i(null);try{const a=w(parseInt(Ze,10)||5,1,60);await d(`/api/debug/camera/fps?fps=${a}`,{method:"POST"});const r=w(parseInt(Qe,10)||8,1,30);await d(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[m,x]=ea.split("x").map(l=>parseInt(l,10));m&&x&&await d(`/api/debug/camera/size?width=${m}&height=${x}`,{method:"POST"}),await d(`/api/debug/camera/sampling?mode=${encodeURIComponent(ta)}`,{method:"POST"}),p(t("cam.notice.runtimeApplied")),G(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},lt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.settingsApplied")),b(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},dt=async()=>{i(null);try{await d("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(c)}),p(t("cam.notice.modeApplied")),b(!1),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},ut=(a,r)=>{if(!a)return;const m=(r==null?void 0:r.syncRuntime)??!0;y({exposure:w(Math.round(j(a.exposure_us,5e3)),100,12e4),gain:w(j(a.analogue_gain,1),1,24),digitalGain:w(j(a.digital_gain,1),1,8),autoExposure:!!(a.auto_exposure??!0),contrast:w(j(a.contrast,1),0,2),brightness:w(j(a.brightness,0),-1,1),saturation:w(j(a.saturation,1),0,2),sharpness:w(j(a.sharpness,1),0,2),noiseReduction:w(Math.round(j(a.noise_reduction,0)),0,4),noiseReductionMode:String(a.noise_reduction_mode??"fast"),aeFlickerMode:String(a.ae_flicker_mode??"off"),autoExposureMaxUs:w(Math.round(j(a.auto_exposure_max_us,2e6)),1e4,1e7),whiteBalanceMode:String(a.white_balance_mode??"auto"),whiteBalanceGainR:w(j(a.white_balance_gain_r,1),.1,3),whiteBalanceGainB:w(j(a.white_balance_gain_b,1),.1,3),colorMode:String(a.color_mode??"color")}),m&&(Xe(String(Math.round(j(a.fps,8)))),aa(`${Math.round(j(a.width,1280))}x${Math.round(j(a.height,720))}`),ra(String(a.sampling_mode??"supersample")),G(!1)),da(w(Math.round(j(a.rotation,180)),0,270)),ua(!!a.flip_horizontal),ma(!!a.flip_vertical),b(!1)},wa=async(a,r)=>{i(null);try{await d("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:a,flip_vertical:r})}),ua(a),ma(r),p(t("cam.notice.mirrorApplied")),await S()}catch(m){i(m instanceof Error?m.message:String(m))}},mt=async a=>{i(null);try{await d(`/api/debug/camera/rotation/${a}`,{method:"POST"}),da(a),p(t("cam.notice.rotationApplied",{value:a})),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},ja=async a=>{i(null);try{await d(`/api/debug/camera/night-mode?enabled=${a?"true":"false"}`,{method:"POST"}),p(t(a?"cam.notice.nightOn":"cam.notice.nightOff")),await S()}catch(r){i(r instanceof Error?r.message:String(r))}},pt=async()=>{if(window.confirm(t("cam.confirm.reset"))){i(null);try{await d("/api/debug/camera/reset",{method:"POST"}),p(t("cam.notice.reset")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}}},xt=async()=>{i(null);try{await d("/api/debug/camera/backup-settings",{method:"POST"}),p(t("cam.notice.backup"))}catch(a){i(a instanceof Error?a.message:String(a))}},ht=async()=>{i(null);try{await d("/api/debug/camera/restore-settings",{method:"POST"}),p(t("cam.notice.restore")),await S()}catch(a){i(a instanceof Error?a.message:String(a))}},fe=async()=>{B(!0);try{const a=await d("/api/debug/camera/presets",{cache:"no-store"});tt(a.presets??[])}catch(a){i(a instanceof Error?a.message:String(a))}finally{B(!1)}},ft=async()=>{const a=Pe.trim();if(a){B(!0),i(null);try{await d("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:a,description:xa.trim(),exposure_us:c.exposure,analogue_gain:c.gain,digital_gain:c.digitalGain,auto_exposure:c.autoExposure,contrast:c.contrast,brightness:c.brightness,saturation:c.saturation,sharpness:c.sharpness,noise_reduction:c.noiseReduction,white_balance_mode:c.whiteBalanceMode,white_balance_gain_r:c.whiteBalanceGainR,white_balance_gain_b:c.whiteBalanceGainB,rotation:la,flip_horizontal:oe,flip_vertical:ie,color_mode:c.colorMode})}),p(t("cam.notice.presetSaved",{name:a})),pa(""),ha(""),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},bt=async a=>{B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}/apply`,{method:"POST"}),p(t("cam.notice.presetApplied",{name:a})),await S(),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}},gt=async a=>{if(window.confirm(t("cam.confirm.deletePreset",{name:a}))){B(!0),i(null);try{await d(`/api/debug/camera/presets/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.presetDeleted",{name:a})),await fe()}catch(r){i(r instanceof Error?r.message:String(r))}finally{B(!1)}}},ze=async()=>{ga(!0);try{const a=await d("/api/debug/files",{cache:"no-store"});rt(a.files??[]),de(1)}catch(a){i(a instanceof Error?a.message:String(a))}finally{ga(!1)}},Ge=()=>{De(null),Ie(null),Oe(!1)},vt=async a=>{if(le===a){Ge();return}De(a),Ie(null),Oe(!0),i(null);try{const r=await d(`/api/debug/files/${encodeURIComponent(a)}/info`,{cache:"no-store"});Ie(r)}catch(r){i(r instanceof Error?r.message:String(r)),De(null)}finally{Oe(!1)}},yt=a=>{const r=(x,l)=>{const f=document.createElement("a");f.href=l,f.download=x,document.body.appendChild(f),f.click(),document.body.removeChild(f)};r(a,`${L("/files")}/${encodeURIComponent(a)}`);const m=a.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(m){const l=`${a.slice(0,-m[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${L("/files")}/${encodeURIComponent(l)}`)).ok)return;r(l,`${L("/files")}/${encodeURIComponent(l)}`),p(t("cam.notice.downloadWithSidecar",{name:a,sidecar:l}))}catch{p(t("cam.notice.download",{name:a}))}})();return}p(t("cam.notice.download",{name:a}))},wt=async a=>{if(window.confirm(t("cam.confirm.deleteFile",{name:a}))){i(null);try{await d(`/api/debug/files/${encodeURIComponent(a)}`,{method:"DELETE"}),p(t("cam.notice.fileDeleted",{name:a})),(le===a||(N==null?void 0:N.filename)===a)&&Ge(),await ze()}catch(r){i(r instanceof Error?r.message:String(r))}}},jt=()=>{if(!na||!C.current||!ue.current)return;const a=C.current;if(!a.naturalWidth||!a.naturalHeight||(Z.current||(Z.current=document.createElement("canvas"),me.current=Z.current.getContext("2d",{willReadFrequently:!0})),pe.current||(pe.current=ue.current.getContext("2d")),!me.current||!pe.current))return;const m=Math.min(1,320/a.naturalWidth),x=Math.max(1,Math.round(a.naturalWidth*m)),l=Math.max(1,Math.round(a.naturalHeight*m));Z.current.width=x,Z.current.height=l,me.current.drawImage(a,0,0,x,l);const f=me.current.getImageData(0,0,x,l).data,F=new Array(256).fill(0),k=new Array(256).fill(0),Q=new Array(256).fill(0),Ue=new Array(256).fill(0);let za=0,Ga=0,qa=0;const U=x*l;for(let E=0;E=250&&(qa+=1)}const Le=U?za/U:0,Et=U?Ga/U-Le*Le:0;et({mean:Le,std:Math.sqrt(Math.max(0,Et)),over:U?qa/U*100:0});const we=ue.current,R=pe.current,je=window.devicePixelRatio||1,Y=Math.max(1,we.clientWidth||240),ee=Math.max(1,we.clientHeight||120);we.width=Math.floor(Y*je),we.height=Math.floor(ee*je),R.setTransform(je,0,0,je,0,0),R.clearRect(0,0,Y,ee);const Mt=Math.max(1,...Me?[Math.max(...F),Math.max(...k),Math.max(...Q)]:[0],...Fe?[Math.max(...Ue)]:[0]),Ne=(E,Se)=>{R.beginPath(),R.strokeStyle=Se,R.lineWidth=1.2;for(let I=0;I<256;I+=1){const ae=I/255*Y,O=ee-E[I]/Mt*ee;I===0?R.moveTo(ae,O):R.lineTo(ae,O)}R.stroke()};if(Me&&(Ne(F,"rgba(255,80,80,0.85)"),Ne(k,"rgba(80,255,80,0.85)"),Ne(Q,"rgba(80,160,255,0.85)")),Fe&&Ne(Ue,"rgba(255,255,255,0.95)"),sa){const E=.9803921568627451*Y;R.fillStyle="rgba(255,100,100,0.12)",R.fillRect(E,0,Y-E,ee)}};s.useEffect(()=>{S(),fe(),ze();const a=window.setInterval(()=>{document.hidden||S()},2e3);return()=>window.clearInterval(a)},[n==null?void 0:n.streaming]),s.useEffect(()=>{!(n!=null&&n.info)||ce||ut(n.info,{syncRuntime:!J})},[n==null?void 0:n.info,ce,J]),s.useEffect(()=>{if(!(n!=null&&n.recording)){P.current&&(window.clearInterval(P.current),P.current=null),ca(0);return}if(P.current)return;const a=Date.now();return P.current=window.setInterval(()=>{ca(Math.max(0,Math.floor((Date.now()-a)/1e3)))},1e3),()=>{P.current&&(window.clearInterval(P.current),P.current=null)}},[n==null?void 0:n.recording]),s.useEffect(()=>{D.current=u,u||X()},[u]),s.useEffect(()=>{u&&(T.current=[],q.current=0)},[te,u]),s.useEffect(()=>{if(!u){T.current=[],q.current=0,se(0);return}if(!He.current){const l=document.createElement("canvas");l.width=32,l.height=32,He.current=l}const r=He.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let m=0;const x=()=>{const l=C.current;if(l!=null&&l.complete&&l.naturalWidth>0)try{r.drawImage(l,0,0,32,32);const f=r.getImageData(0,0,32,32).data;let F=2166136261;for(let k=0;kwindow.cancelAnimationFrame(m)},[u]),s.useEffect(()=>{if(!u){se(0);return}const a=window.setInterval(()=>{const m=performance.now()-1e3,x=T.current;for(;x.length&&x[0]window.clearInterval(a)},[u]),s.useEffect(()=>{if(!u){Be(null);return}let a=!1;const r=async()=>{try{const x=await fetch(`${L("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!x.ok||a)return;const l=await x.json();if(!a){Be(l);const f=Number(l.preview_target_fps??l.target_preview_fps??0);Number.isFinite(f)&&f>0&&!J&&Ye(String(Math.round(f)))}}catch{a||Be(null)}};r();const m=window.setInterval(r,2e3);return()=>{a=!0,window.clearInterval(m)}},[u,J]),s.useEffect(()=>{if(!V)return;const a=window.setTimeout(()=>p(null),3200);return()=>window.clearTimeout(a)},[V]),s.useEffect(()=>()=>X(),[]),s.useEffect(()=>{const a=Math.max(1,Math.ceil(K.length/ke));Ae>a&&de(a)},[K.length,Ae]);const Nt=u?`${L("/camera/stream")}?t=${te}`:"",be=c.autoExposure,qe=c.whiteBalanceMode==="manual",A=((Ra=n==null?void 0:n.info)==null?void 0:Ra.capabilities)??{},St=Array.isArray(A.awb_modes)&&A.awb_modes.length>0?A.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],kt=Array.isArray(A.noise_reduction_modes)&&A.noise_reduction_modes.length>0?A.noise_reduction_modes:["off","fast","high_quality"],Na=A.manual_digital_gain!==!1,Sa=!!((Ea=n==null?void 0:n.info)!=null&&Ea.night_mode),ka=u,ge=!M&&!u&&!(n!=null&&n.recording),_a=!M&&u,_t=!M&&!Je&&ka,Ct=!M&&!Ee&&ka,ve=Math.max(1,Math.ceil(K.length/ke)),ye=Math.min(Ae,ve),Ca=(ye-1)*ke,Rt=K.slice(Ca,Ca+ke);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${t("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("zh"),children:t("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>v("en"),children:t("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Wt,{className:"h-3.5 w-3.5"})," ",t("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Qt.map(a=>e.jsxs("button",{type:"button",onClick:()=>void mt(a),className:`rounded border px-2 py-1 ${la===a?"border-primary text-primary":"border-outline-variant/30"}`,children:[a,"°"]},a))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void wa(!oe,ie),className:`rounded border px-2 py-1 ${oe?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void wa(oe,!ie),className:`rounded border px-2 py-1 ${ie?"border-primary text-primary":"border-outline-variant/30"}`,children:t("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void xt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ot,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ht(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(qt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1",children:t("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:t("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ja(!0),className:`rounded border px-2 py-1 ${Sa?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Lt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ja(!1),className:`rounded border px-2 py-1 ${Sa?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Kt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:Pe,onChange:a=>pa(a.target.value),placeholder:t("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:xa,onChange:a=>ha(a.target.value),placeholder:t("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:Te||!Pe.trim(),onClick:()=>void ft(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[fa.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.presets.empty")}),fa.map(a=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:a.name}),e.jsx("div",{className:"text-on-surface-variant",children:a.description||t("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[t("cam.controls.exposure"),": ",a.exposure_us,"us | ",t("cam.controls.gain"),": ",a.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:Te,onClick:()=>void bt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:Te,onClick:()=>void gt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.presets.delete")})]})]},a.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.cpu_usage)==null?"—":`${Number(h.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.memory_usage)==null?"—":`${Number(h.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(h==null?void 0:h.temperature)==null?"—":`${Number(h.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:t("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"shrink-0 font-mono text-xs text-on-surface-variant",children:[t("cam.preview.state"),": ",t(u?"cam.state.streaming":"cam.state.idle")]})]}),re&&!u&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:"relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black",children:[u&&re&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:re}),We?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:t("cam.err.streamProbeDetail")})]}),u?e.jsx("img",{ref:C,alt:"camera-preview",className:"h-full w-full object-contain",src:Nt,onLoad:()=>{H(null),z(!1),jt()},onError:()=>{var r;if(X(),!D.current)return;const a=(r=C.current)==null?void 0:r.src;(async()=>{if(!D.current||!a)return;if(await Ja()==="busy"){H(t("cam.err.streamBusy")),z(!0);return}xe.current=window.setTimeout(()=>{D.current&&Ce(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ua,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:t("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:t("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"mr-1 inline h-3.5 w-3.5"}),t(M?"cam.btn.starting":"cam.btn.start")]})]}),(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(La,{className:"h-3.5 w-3.5 fill-current"})," ",t("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(ia/60).toString().padStart(2,"0")}:${(ia%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Ya(a=>!a),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:t(oa?"cam.hist.expand":"cam.hist.collapse")})}),!oa&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:na,onChange:a=>Ka(a.target.checked)})," ",t("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Me,onChange:a=>Za(a.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Fe,onChange:a=>Xa(a.target.checked)})," ",t("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:sa,onChange:a=>Qa(a.target.checked)})," ",t("cam.hist.over")]})]}),e.jsx("canvas",{ref:ue,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:$e.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:$e.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[$e.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_preview_fps)??at).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((Ma=n==null?void 0:n.info)==null?void 0:Ma.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(o==null?void 0:o.preview_target_fps)??(o==null?void 0:o.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:t("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((o==null?void 0:o.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((o==null?void 0:o.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((o==null?void 0:o.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(o==null?void 0:o.jpeg_source_format)??"RGB888"} / fail ${(o==null?void 0:o.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(o==null?void 0:o.preview_consumers)??0}/${(o==null?void 0:o.analysis_consumers)??0}/${(o==null?void 0:o.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((o==null?void 0:o.process_rss_kb)??0)/1024)} / ${Math.round(Number((o==null?void 0:o.process_swap_kb)??0)/1024)} MB`})]}),(o==null?void 0:o.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:t("cam.stats.longExposureThrottle",{exposure:Math.round(Number(o.actual_exposure_us??0)/1e3)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:he.current!=null?`${Math.max(0,Math.round((performance.now()-he.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((Fa=n==null?void 0:n.info)==null?void 0:Fa.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String((($a=n==null?void 0:n.info)==null?void 0:$a.driver)??(o==null?void 0:o.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Ba=n==null?void 0:n.info)==null?void 0:Ba.backend)??(o==null?void 0:o.camera_backend)??"—"} · lores ${(Pa=n==null?void 0:n.info)!=null&&Pa.lores_available||o!=null&&o.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((Ta=n==null?void 0:n.info)==null?void 0:Ta.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((Ia=n==null?void 0:n.info)==null?void 0:Ia.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Oa=n==null?void 0:n.info)==null?void 0:Oa.width)??"—"}x${((Da=n==null?void 0:n.info)==null?void 0:Da.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[t("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Aa=n==null?void 0:n.info)!=null&&Aa.auto_exposure?t("cam.controls.auto"):t("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!ge,onClick:()=>void ya(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${ge?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wa,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!_a,onClick:()=>void st(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${_a?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Jt,{className:"inline h-4 w-4"})," ",t(M?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!_t,onClick:()=>void ot(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ua,{className:"inline h-4 w-4"})," ",t(Je?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!Ct,onClick:()=>void it(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(La,{className:"inline h-4 w-4"})," ",Ee?t("cam.btn.recordBusy"):n!=null&&n.recording?t("cam.btn.recordStop"):t("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:ba,onClick:()=>void ze(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.refresh")})}),nt&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:t("cam.files.loadingInfo")}),N&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Gt,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:N.filename})]}),e.jsx("button",{type:"button",onClick:()=>Ge(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":t("cam.files.closeDetail"),children:e.jsx(Dt,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[t("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Va(N.size)})]}),e.jsxs("div",{children:[t("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:N.type})]}),e.jsxs("div",{children:[t("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(N.modified).toLocaleString()})]}),N.exposure_us!=null&&e.jsxs("div",{children:[t("cam.controls.exposure"),": ",e.jsxs("span",{className:"font-mono",children:[N.exposure_us,"us"]})]}),N.analogue_gain!=null&&e.jsxs("div",{children:[t("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:N.analogue_gain})]}),N.resolution&&e.jsxs("div",{children:[t("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:N.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[K.length===0&&!ba&&e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.empty")}),Rt.map(a=>e.jsxs("div",{className:`rounded border p-2 ${le===a.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:a.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Va(a.size)," | ",new Date(a.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:a.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>yt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(zt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void vt(a.name),className:`rounded border px-2 py-1 ${le===a.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Ut,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void wt(a.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Pt,{className:"mr-1 inline h-3.5 w-3.5"}),t("cam.files.delete")]})]})]},a.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ye<=1,onClick:()=>de(a=>Math.max(1,a-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:t("cam.files.page",{current:ye,total:ve})}),e.jsx("button",{type:"button",disabled:ye>=ve,onClick:()=>de(a=>Math.min(ve,a+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:t("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.sensorFps"),e.jsx("input",{value:Ze,onChange:a=>{Xe(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.previewFps"),e.jsx("input",{value:Qe,onChange:a=>{Ye(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[t("cam.controls.sampling"),e.jsxs("select",{value:ta,onChange:a=>{ra(a.target.value),G(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[t("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Xt.map(a=>e.jsx("button",{type:"button",onClick:()=>{aa(a),G(!0)},className:`rounded border px-2 py-1 ${ea===a?"border-primary text-primary":"border-outline-variant/30"}`,children:a},a))})]}),e.jsx("button",{type:"button",disabled:!J,onClick:()=>void ct(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Vt,{className:"h-3.5 w-3.5"})," ",t("cam.controls.core")]}),ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:t("cam.controls.pendingChanges")}),be&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx($,{label:t("cam.controls.exposure"),value:c.exposure,min:100,max:12e4,step:100,unit:"us",disabled:be,onChange:a=>{b(!0),y(r=>({...r,exposure:a}))}}),e.jsx($,{label:t("cam.controls.gain"),value:c.gain,min:1,max:24,step:.1,disabled:be,onChange:a=>{b(!0),y(r=>({...r,gain:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.digitalGain"),value:c.digitalGain,min:1,max:8,step:.1,disabled:be||!Na,onChange:a=>{b(!0),y(r=>({...r,digitalGain:Number(a.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[t("cam.controls.noiseReductionMode"),e.jsx("select",{value:c.noiseReductionMode,onChange:a=>{b(!0),y(r=>({...r,noiseReductionMode:a.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:kt.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.nr.${a}`)},a))})]}),e.jsx($,{label:t("cam.controls.contrast"),value:c.contrast,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,contrast:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.brightness"),value:c.brightness,min:-1,max:1,step:.1,onChange:a=>{b(!0),y(r=>({...r,brightness:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.saturation"),value:c.saturation,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,saturation:Number(a.toFixed(1))}))}}),e.jsx($,{label:t("cam.controls.sharpness"),value:c.sharpness,min:0,max:2,step:.1,onChange:a=>{b(!0),y(r=>({...r,sharpness:Number(a.toFixed(1))}))}})]}),!Na&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[t("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ha=n==null?void 0:n.info)==null?void 0:Ha.actual_digital_gain)??c.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!ce,onClick:()=>void lt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:t("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:t("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[t("cam.controls.autoExposure"),e.jsxs("select",{value:c.autoExposure?"auto":"manual",onChange:a=>{y(r=>({...r,autoExposure:a.target.value==="auto"})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:t("cam.controls.auto")}),e.jsx("option",{value:"manual",children:t("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.colorMode"),e.jsxs("select",{value:c.colorMode,onChange:a=>{y(r=>({...r,colorMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:t("cam.controls.color")}),e.jsx("option",{value:"mono",children:t("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.whiteBalance"),e.jsx("select",{value:c.whiteBalanceMode,onChange:a=>{y(r=>({...r,whiteBalanceMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:St.map(a=>e.jsx("option",{value:a,children:t(`cam.controls.wb.${a}`)},a))})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.aeFlicker"),e.jsxs("select",{value:c.aeFlickerMode,onChange:a=>{y(r=>({...r,aeFlickerMode:a.target.value})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:t("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[t("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:1e4,max:1e7,step:1e4,value:c.autoExposureMaxUs,onChange:a=>{y(r=>({...r,autoExposureMaxUs:w(parseInt(a.target.value,10)||2e6,1e4,1e7)})),b(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx($,{label:"R Gain",value:c.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainR:Number(a.toFixed(1))})),b(!0)}}),e.jsx($,{label:"B Gain",value:c.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!qe,onChange:a=>{y(r=>({...r,whiteBalanceGainB:Number(a.toFixed(1))})),b(!0)}})]}),!qe&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:t("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void dt(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:t("cam.controls.applyMode")})})]})]})]})]}),(Re||V)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[Re&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:Re}),V&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:V})]})]})}Ft.createRoot(document.getElementById("root")).render(e.jsx($t.StrictMode,{children:e.jsx(Tt,{children:e.jsx(Ht,{children:e.jsx(Yt,{})})})})); diff --git a/web/static/analysis-lab/assets/camera-l9M7WKlh.js b/web/static/analysis-lab/assets/camera-l9M7WKlh.js new file mode 100644 index 0000000..e75ee24 --- /dev/null +++ b/web/static/analysis-lab/assets/camera-l9M7WKlh.js @@ -0,0 +1,61 @@ +import{r as s,j as e,a as ur,R as mr}from"./client-D1ZVDB-N.js";import{c as P,u as xr,T as pr,I as hr}from"./index-DfDaIsmY.js";import{u as fr,S as br,C as ja,X as gr,r as vr,b as yr}from"./http-BxpNn71N.js";import{R as jr}from"./rotate-ccw-B0-EEdqV.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 wa=P("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** + * @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 xt=P("Crosshair",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]]);/** + * @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 wr=P("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @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 Nr=P("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** + * @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 kr=P("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** + * @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 Sr=P("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** + * @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 _r=P("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @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 Na=P("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @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 Cr=P("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** + * @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 Fr=P("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** + * @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 Rr=P("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** + * @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 $r=P("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Er="/api/dev/debug";function X(a){return`${Er}${a}`}async function h(a,j={}){const g=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return vr(g,j)}const Br=["640x360","1280x720","1600x900","1920x1020"],Pr=[0,90,180,270],We=12;function S(a,j,g){return Math.min(g,Math.max(j,a))}function _(a,j){return a==null||Number.isNaN(Number(a))?j:Number(a)}function ka(a){if(!a)return"0 B";const j=["B","KB","MB","GB"];let g=0,f=a;for(;f>=1024&&gz.hfd),f=Math.min(...g),n=Math.max(...g),ne=Math.max(.2,n-f),x=g.map((z,se)=>{const oe=se/Math.max(1,g.length-1)*320,c=76-(z-f)/ne*60;return`${oe.toFixed(1)},${c.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:x,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((D=x.split(" ").at(-1))==null?void 0:D.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Sa(){try{const a=await fetch(`${X("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const j=await a.json(),g=Number(j.max_clients??0),f=Number(j.active_clients??0);return g>0&&f>=g?"busy":"ok"}catch{return"fail"}}function O({label:a,value:j,min:g,max:f,step:n,onChange:ne,disabled:x=!1,unit:D=""}){return e.jsxs("label",{className:`block ${x?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[j.toFixed(n>=1?0:n>=.1?1:2),D]})]}),e.jsx("input",{type:"range",min:g,max:f,step:n,value:j,disabled:x,onChange:z=>ne(Number(z.target.value)),className:"w-full accent-primary"})]})}function Tr(){var na,sa,oa,ia,ca,la,da,ua,ma,xa,pa,ha,fa,ba,ga,va;const{t:a,locale:j,setLocale:g}=xr(),{info:f}=fr(),[n,ne]=s.useState(null),[x,D]=s.useState(!1),[z,se]=s.useState(()=>Date.now()),[oe,c]=s.useState(null),[be,Y]=s.useState(null),[pt,Z]=s.useState(!1),[ie,b]=s.useState(null),[M,ge]=s.useState(!1),[Le,ht]=s.useState(!1),[ft,bt]=s.useState(!1),[gt,vt]=s.useState("8"),[yt,jt]=s.useState("8"),[wt,Nt]=s.useState("1280x720"),[kt,St]=s.useState("supersample"),[ce,Q]=s.useState(!1),[_t,_a]=s.useState(!0),[Ue,Ca]=s.useState(!0),[Ve,Fa]=s.useState(!1),[Ct,Ra]=s.useState(!1),[Ft,Rt]=s.useState(!1),[Je,$a]=s.useState({mean:0,std:0,over:0}),[Ea,ve]=s.useState(0),[i,Ke]=s.useState(null),[k,Xe]=s.useState(!1),[ye,$t]=s.useState(!1),[o,Et]=s.useState(null),[je,le]=s.useState([]),[ee,de]=s.useState(null),[y,Ye]=s.useState(null),[Bt,V]=s.useState(null),[Pt,Mt]=s.useState(0),[Tt,It]=s.useState(180),[we,Ht]=s.useState(!1),[Ne,At]=s.useState(!1),[u,N]=s.useState({exposure:5e3,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxUs:2e6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[ke,w]=s.useState(!1),[Ze,Ot]=s.useState(""),[Dt,zt]=s.useState(""),[Gt,Ba]=s.useState([]),[Qe,G]=s.useState(!1),[ue,Pa]=s.useState([]),[qt,Wt]=s.useState(!1),[C,et]=s.useState(null),[Ma,tt]=s.useState(!1),[Se,at]=s.useState(null),[rt,_e]=s.useState(1),E=s.useRef(null),Ce=s.useRef(null),me=s.useRef(null),Fe=s.useRef(null),Re=s.useRef(null),q=s.useRef(null),$e=s.useRef(null),nt=s.useRef(!1),J=s.useRef(!1),Ee=s.useRef(null),W=s.useRef([]),te=s.useRef(0),st=s.useRef(null),Lt=()=>{W.current=[],te.current=0,Ee.current=null,ve(0)},R=async()=>{try{const t=await h("/api/debug/camera/status",{cache:"no-store"});ne(t),t.streaming||(D(!1),J.current=!1)}catch(t){c(t instanceof Error?t.message:String(t))}},xe=()=>{$e.current!=null&&(window.clearTimeout($e.current),$e.current=null)},ot=async()=>{if(M)return!1;ge(!0),c(null),Y(null),Z(!1);try{xe(),n!=null&&n.streaming||await h("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Sa();return r==="busy"?(Y(a("cam.err.streamBusy")),Z(!0),!1):r==="fail"?(Y(a("cam.err.streamProbeFailed")),Z(!1),!1):(D(!0),J.current=!0,b(a("cam.notice.previewStart")),Lt(),Ee.current=performance.now(),se(t),await R(),!0)}catch(t){return c(t instanceof Error?t.message:String(t)),!1}finally{ge(!1)}},Ut=async()=>{if(!ye){$t(!0),V(null);try{if(!(x||await ot()))return;Et(null),le([]),de(null),Ye(null),Rt(!0),Xe(!0),window.setTimeout(()=>{var r;(r=document.getElementById("focus-calibration-panel"))==null||r.scrollIntoView({behavior:"smooth",block:"start"})},0)}finally{$t(!1)}}},Vt=()=>{Xe(!1),V(null)},Ta=()=>{le([]),de(null),V(null)},Ia=t=>{if(!k||!E.current||t.target.closest("button, label, input"))return;const r=E.current,l=t.currentTarget.getBoundingClientRect(),p=r.naturalWidth||(o==null?void 0:o.frame.width)||0,d=r.naturalHeight||(o==null?void 0:o.frame.height)||0;if(p<=0||d<=0)return;const m=Math.min(l.width/p,l.height/d),F=p*m,v=d*m,$=(l.width-F)/2,B=(l.height-v)/2,A=(t.clientX-l.left-$)/F,ae=(t.clientY-l.top-B)/v;A<0||A>1||ae<0||ae>1||(Ye({x:A,y:ae}),le([]),de(null),V(null))},Ha=()=>{Ye(null),le([]),de(null),V(null)},Aa=async()=>{if(!M){ge(!0),c(null);try{xe(),D(!1),J.current=!1,Y(null),Z(!1),Lt(),E.current&&(E.current.onload=null,E.current.onerror=null,E.current.src="",E.current.removeAttribute("src")),se(Date.now()),b(a("cam.notice.previewStop"))}catch(t){c(t instanceof Error?t.message:String(t))}finally{ge(!1)}}},Oa=async()=>{if(!(!x&&!(n!=null&&n.streaming))){bt(!0),c(null);try{const t=await h("/api/debug/camera/capture",{method:"POST"});b(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){c(t instanceof Error?t.message:String(t))}finally{bt(!1)}}},Da=async()=>{if(!Le){ht(!0),c(null);try{if(n!=null&&n.recording)await h("/api/debug/camera/record/stop",{method:"POST"}),b(a("cam.notice.recordStop"));else{const t=await h("/api/debug/camera/record/start",{method:"POST"});b(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){c(t instanceof Error?t.message:String(t))}finally{ht(!1)}}},za=async()=>{c(null);try{const t=S(parseInt(gt,10)||5,1,60);await h(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=S(parseInt(yt,10)||8,1,30);await h(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[l,p]=wt.split("x").map(d=>parseInt(d,10));l&&p&&await h(`/api/debug/camera/size?width=${l}&height=${p}`,{method:"POST"}),await h(`/api/debug/camera/sampling?mode=${encodeURIComponent(kt)}`,{method:"POST"}),b(a("cam.notice.runtimeApplied")),Q(!1),await R()}catch(t){c(t instanceof Error?t.message:String(t))}},Ga=async()=>{c(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)}),b(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){c(t instanceof Error?t.message:String(t))}},qa=async()=>{c(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)}),b(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){c(t instanceof Error?t.message:String(t))}},Wa=(t,r)=>{if(!t)return;const l=(r==null?void 0:r.syncRuntime)??!0;N({exposure:S(Math.round(_(t.exposure_us,5e3)),100,12e4),gain:S(_(t.analogue_gain,1),1,24),digitalGain:S(_(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:S(_(t.contrast,1),0,2),brightness:S(_(t.brightness,0),-1,1),saturation:S(_(t.saturation,1),0,2),sharpness:S(_(t.sharpness,1),0,2),noiseReduction:S(Math.round(_(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxUs:S(Math.round(_(t.auto_exposure_max_us,2e6)),1e4,1e7),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:S(_(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:S(_(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),l&&(vt(String(Math.round(_(t.fps,8)))),Nt(`${Math.round(_(t.width,1280))}x${Math.round(_(t.height,720))}`),St(String(t.sampling_mode??"supersample")),Q(!1)),It(S(Math.round(_(t.rotation,180)),0,270)),Ht(!!t.flip_horizontal),At(!!t.flip_vertical),w(!1)},Jt=async(t,r)=>{c(null);try{await h("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),Ht(t),At(r),b(a("cam.notice.mirrorApplied")),await R()}catch(l){c(l instanceof Error?l.message:String(l))}},La=async t=>{c(null);try{await h(`/api/debug/camera/rotation/${t}`,{method:"POST"}),It(t),b(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){c(r instanceof Error?r.message:String(r))}},Kt=async t=>{c(null);try{await h(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),b(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){c(r instanceof Error?r.message:String(r))}},Ua=async()=>{if(window.confirm(a("cam.confirm.reset"))){c(null);try{await h("/api/debug/camera/reset",{method:"POST"}),b(a("cam.notice.reset")),await R()}catch(t){c(t instanceof Error?t.message:String(t))}}},Va=async()=>{c(null);try{await h("/api/debug/camera/backup-settings",{method:"POST"}),b(a("cam.notice.backup"))}catch(t){c(t instanceof Error?t.message:String(t))}},Ja=async()=>{c(null);try{await h("/api/debug/camera/restore-settings",{method:"POST"}),b(a("cam.notice.restore")),await R()}catch(t){c(t instanceof Error?t.message:String(t))}},Be=async()=>{G(!0);try{const t=await h("/api/debug/camera/presets",{cache:"no-store"});Ba(t.presets??[])}catch(t){c(t instanceof Error?t.message:String(t))}finally{G(!1)}},Ka=async()=>{const t=Ze.trim();if(t){G(!0),c(null);try{await h("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Dt.trim(),exposure_us:u.exposure,analogue_gain:u.gain,digital_gain:u.digitalGain,auto_exposure:u.autoExposure,contrast:u.contrast,brightness:u.brightness,saturation:u.saturation,sharpness:u.sharpness,noise_reduction:u.noiseReduction,white_balance_mode:u.whiteBalanceMode,white_balance_gain_r:u.whiteBalanceGainR,white_balance_gain_b:u.whiteBalanceGainB,rotation:Tt,flip_horizontal:we,flip_vertical:Ne,color_mode:u.colorMode})}),b(a("cam.notice.presetSaved",{name:t})),Ot(""),zt(""),await Be()}catch(r){c(r instanceof Error?r.message:String(r))}finally{G(!1)}}},Xa=async t=>{G(!0),c(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),b(a("cam.notice.presetApplied",{name:t})),await R(),await Be()}catch(r){c(r instanceof Error?r.message:String(r))}finally{G(!1)}},Ya=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){G(!0),c(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),b(a("cam.notice.presetDeleted",{name:t})),await Be()}catch(r){c(r instanceof Error?r.message:String(r))}finally{G(!1)}}},it=async()=>{Wt(!0);try{const t=await h("/api/debug/files",{cache:"no-store"});Pa(t.files??[]),_e(1)}catch(t){c(t instanceof Error?t.message:String(t))}finally{Wt(!1)}},ct=()=>{at(null),et(null),tt(!1)},Za=async t=>{if(Se===t){ct();return}at(t),et(null),tt(!0),c(null);try{const r=await h(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});et(r)}catch(r){c(r instanceof Error?r.message:String(r)),at(null)}finally{tt(!1)}},Qa=t=>{const r=(p,d)=>{const m=document.createElement("a");m.href=d,m.download=p,document.body.appendChild(m),m.click(),document.body.removeChild(m)};r(t,`${X("/files")}/${encodeURIComponent(t)}`);const l=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(l){const d=`${t.slice(0,-l[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${X("/files")}/${encodeURIComponent(d)}`)).ok)return;r(d,`${X("/files")}/${encodeURIComponent(d)}`),b(a("cam.notice.downloadWithSidecar",{name:t,sidecar:d}))}catch{b(a("cam.notice.download",{name:t}))}})();return}b(a("cam.notice.download",{name:t}))},er=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){c(null);try{await h(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),b(a("cam.notice.fileDeleted",{name:t})),(Se===t||(C==null?void 0:C.filename)===t)&&ct(),await it()}catch(r){c(r instanceof Error?r.message:String(r))}}},tr=()=>{if(!_t||!E.current||!Ce.current)return;const t=E.current;if(!t.naturalWidth||!t.naturalHeight||(me.current||(me.current=document.createElement("canvas"),Fe.current=me.current.getContext("2d",{willReadFrequently:!0})),Re.current||(Re.current=Ce.current.getContext("2d")),!Fe.current||!Re.current))return;const l=Math.min(1,320/t.naturalWidth),p=Math.max(1,Math.round(t.naturalWidth*l)),d=Math.max(1,Math.round(t.naturalHeight*l));me.current.width=p,me.current.height=d,Fe.current.drawImage(t,0,0,p,d);const m=Fe.current.getImageData(0,0,p,d).data,F=new Array(256).fill(0),v=new Array(256).fill(0),$=new Array(256).fill(0),B=new Array(256).fill(0);let A=0,ae=0,ya=0;const re=p*d;for(let I=0;I=250&&(ya+=1)}const mt=re?A/re:0,lr=re?ae/re-mt*mt:0;$a({mean:mt,std:Math.sqrt(Math.max(0,lr)),over:re?ya/re*100:0});const De=Ce.current,T=Re.current,ze=window.devicePixelRatio||1,pe=Math.max(1,De.clientWidth||240),he=Math.max(1,De.clientHeight||120);De.width=Math.floor(pe*ze),De.height=Math.floor(he*ze),T.setTransform(ze,0,0,ze,0,0),T.clearRect(0,0,pe,he);const dr=Math.max(1,...Ue?[Math.max(...F),Math.max(...v),Math.max(...$)]:[0],...Ve?[Math.max(...B)]:[0]),Ge=(I,qe)=>{T.beginPath(),T.strokeStyle=qe,T.lineWidth=1.2;for(let L=0;L<256;L+=1){const fe=L/255*pe,U=he-I[L]/dr*he;L===0?T.moveTo(fe,U):T.lineTo(fe,U)}T.stroke()};if(Ue&&(Ge(F,"rgba(255,80,80,0.85)"),Ge(v,"rgba(80,255,80,0.85)"),Ge($,"rgba(80,160,255,0.85)")),Ve&&Ge(B,"rgba(255,255,255,0.95)"),Ct){const I=.9803921568627451*pe;T.fillStyle="rgba(255,100,100,0.12)",T.fillRect(I,0,pe-I,he)}};s.useEffect(()=>{R(),Be(),it();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),s.useEffect(()=>{!(n!=null&&n.info)||ke||Wa(n.info,{syncRuntime:!ce})},[n==null?void 0:n.info,ke,ce]),s.useEffect(()=>{if(!(n!=null&&n.recording)){q.current&&(window.clearInterval(q.current),q.current=null),Mt(0);return}if(q.current)return;const t=Date.now();return q.current=window.setInterval(()=>{Mt(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{q.current&&(window.clearInterval(q.current),q.current=null)}},[n==null?void 0:n.recording]),s.useEffect(()=>{J.current=x,x||(xe(),Xe(!1))},[x]),s.useEffect(()=>{if(!k||!x)return;let t=!1;const r=async()=>{var p,d;if(!(nt.current||document.hidden)){nt.current=!0;try{const m=new URLSearchParams;y&&(m.set("target_x",y.x.toFixed(6)),m.set("target_y",y.y.toFixed(6)));const F=m.size>0?`?${m.toString()}`:"",v=await h(`${X("/camera/focus/metrics")}${F}`,{cache:"no-store"});if(t)return;Et(v),V(null);const $=y?(p=v.selected_star)==null?void 0:p.hfd_px:(d=v.aggregate)==null?void 0:d.median_hfd_px;typeof $=="number"&&Number.isFinite($)&&(le(B=>{var A;return((A=B.at(-1))==null?void 0:A.frameId)===v.frame_id?B:[...B,{frameId:v.frame_id,hfd:$}].slice(-60)}),de(B=>B==null?$:Math.min(B,$)))}catch(m){t||V(m instanceof Error?m.message:String(m))}finally{nt.current=!1}}};r();const l=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(l)}},[k,x,y==null?void 0:y.x,y==null?void 0:y.y]),s.useEffect(()=>{x&&(W.current=[],te.current=0)},[z,x]),s.useEffect(()=>{if(!x){W.current=[],te.current=0,ve(0);return}if(!st.current){const d=document.createElement("canvas");d.width=32,d.height=32,st.current=d}const r=st.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let l=0;const p=()=>{const d=E.current;if(d!=null&&d.complete&&d.naturalWidth>0)try{r.drawImage(d,0,0,32,32);const m=r.getImageData(0,0,32,32).data;let F=2166136261;for(let v=0;vwindow.cancelAnimationFrame(l)},[x]),s.useEffect(()=>{if(!x){ve(0);return}const t=window.setInterval(()=>{const l=performance.now()-1e3,p=W.current;for(;p.length&&p[0]window.clearInterval(t)},[x]),s.useEffect(()=>{if(!x){Ke(null);return}let t=!1;const r=async()=>{try{const p=await fetch(`${X("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!p.ok||t)return;const d=await p.json();if(!t){Ke(d);const m=Number(d.preview_target_fps??d.target_preview_fps??0);Number.isFinite(m)&&m>0&&!ce&&jt(String(Math.round(m)))}}catch{t||Ke(null)}};r();const l=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(l)}},[x,ce]),s.useEffect(()=>{if(!ie)return;const t=window.setTimeout(()=>b(null),3200);return()=>window.clearTimeout(t)},[ie]),s.useEffect(()=>()=>xe(),[]),s.useEffect(()=>{const t=Math.max(1,Math.ceil(ue.length/We));rt>t&&_e(t)},[ue.length,rt]);const ar=x?`${X("/camera/stream")}?t=${z}`:"",Pe=u.autoExposure,lt=u.whiteBalanceMode==="manual",K=((na=n==null?void 0:n.info)==null?void 0:na.capabilities)??{},rr=Array.isArray(K.awb_modes)&&K.awb_modes.length>0?K.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],nr=Array.isArray(K.noise_reduction_modes)&&K.noise_reduction_modes.length>0?K.noise_reduction_modes:["off","fast","high_quality"],Xt=K.manual_digital_gain!==!1,Yt=!!((sa=n==null?void 0:n.info)!=null&&sa.night_mode),Zt=x,Me=!M&&!x&&!(n!=null&&n.recording),Qt=!M&&x,sr=!M&&!ft&&Zt,or=!M&&!Le&&Zt,H=y?o==null?void 0:o.selected_star:null,Te=y?(H==null?void 0:H.hfd_px)??null:((oa=o==null?void 0:o.aggregate)==null?void 0:oa.median_hfd_px)??null,ea=y?(H==null?void 0:H.fwhm_px)??null:((ia=o==null?void 0:o.aggregate)==null?void 0:ia.median_fwhm_px)??null,ta=y?(H==null?void 0:H.concentration)??null:((ca=o==null?void 0:o.aggregate)==null?void 0:ca.median_concentration)??null,Ie=Te!=null&&ee!=null&&ee>0?(Te-ee)/ee*100:null,aa=je.slice(-3).map(t=>t.hfd),dt=je.slice(-6,-3).map(t=>t.hfd),ut=t=>t.reduce((r,l)=>r+l,0)/Math.max(1,t.length);let He="cam.focus.guidance.collecting";if((o==null?void 0:o.state)==="no_stars")He="cam.focus.guidance.noStars";else if(je.length>=5&&Ie!=null&&Ie<=3)He="cam.focus.guidance.best";else if(aa.length===3&&dt.length===3){const t=ut(aa)-ut(dt),r=Math.max(.04,ut(dt)*.02);He=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const ir=((o==null?void 0:o.stars_used)??0)>=5?"cam.focus.confidence.high":((o==null?void 0:o.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Ae=Math.max(1,Math.ceil(ue.length/We)),Oe=Math.min(rt,Ae),ra=(Oe-1)*We,cr=ue.slice(ra,ra+We);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${j==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${j==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Cr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Pr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void La(t),className:`rounded border px-2 py-1 ${Tt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void Jt(!we,Ne),className:`rounded border px-2 py-1 ${we?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void Jt(we,!Ne),className:`rounded border px-2 py-1 ${Ne?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void Va(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(br,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void Ja(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(kr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void Ua(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void Kt(!0),className:`rounded border px-2 py-1 ${Yt?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(_r,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void Kt(!1),className:`rounded border px-2 py-1 ${Yt?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx($r,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:Ze,onChange:t=>Ot(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Dt,onChange:t=>zt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:Qe||!Ze.trim(),onClick:()=>void Ka(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Gt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Gt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",t.exposure_us,"us | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:Qe,onClick:()=>void Xa(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:Qe,onClick:()=>void Ya(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.cpu_usage)==null?"—":`${Number(f.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.memory_usage)==null?"—":`${Number(f.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.temperature)==null?"—":`${Number(f.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:ye||M||!!(n!=null&&n.recording),onClick:()=>k?Vt():void Ut(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${k?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(xt,{className:"mr-1 inline h-3.5 w-3.5"}),a(k?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(x?"cam.state.streaming":"cam.state.idle")]})]})]}),be&&!x&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:be}),pt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${k?"cursor-crosshair":""}`,onClick:Ia,children:[x&&be&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:be}),pt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),x?e.jsx("img",{ref:E,alt:"camera-preview",className:"h-full w-full object-contain",src:ar,onLoad:()=>{Y(null),Z(!1),tr()},onError:()=>{var r;if(xe(),!J.current)return;const t=(r=E.current)==null?void 0:r.src;(async()=>{if(!J.current||!t)return;if(await Sa()==="busy"){Y(a("cam.err.streamBusy")),Z(!0);return}$e.current=window.setTimeout(()=>{J.current&&se(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(ja,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!Me,onClick:()=>void ot(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${Me?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Na,{className:"mr-1 inline h-3.5 w-3.5"}),a(M?"cam.btn.starting":"cam.btn.start")]})]}),x&&k&&(o!=null&&o.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${o.frame.width} ${o.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[o.stars.map((t,r)=>{const l=o.selected_star&&Math.abs(o.selected_star.x-t.x)<1&&Math.abs(o.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:l?16:10,fill:"none",stroke:l?"#facc15":"#38bdf8",strokeWidth:l?3:2,vectorEffect:"non-scaling-stroke"},`${o.frame_id}-${r}`)}),y?e.jsxs("g",{transform:`translate(${y.x*o.frame.width} ${y.y*o.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(wa,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Pt/60).toString().padStart(2,"0")}:${(Pt%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Rt(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(Ft?"cam.hist.expand":"cam.hist.collapse")})}),!Ft&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:_t,onChange:t=>_a(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ue,onChange:t=>Ca(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ve,onChange:t=>Fa(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ct,onChange:t=>Ra(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Ce,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:Je.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:Je.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[Je.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${k?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(xt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${k?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(k?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[k?e.jsx("button",{type:"button",onClick:Vt,className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:ye||M||!!(n!=null&&n.recording),onClick:()=>void Ut(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(xt,{className:"mr-1 inline h-4 w-4"}),a(ye?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!k,onClick:Ta,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(jr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!k||y==null,onClick:Ha,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),k?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(y?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:Te==null?"—":Te.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(He)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:ee==null?"—":`${ee.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ie==null?"—":`+${Math.max(0,Ie).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(o==null?void 0:o.stars_used)??0," / ",(o==null?void 0:o.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(ir)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ea==null?"—":`${ea.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ta==null?"—":`${(ta*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:o!=null&&o.selected_star?`HFD ${o.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(y?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:o!=null&&o.aggregate?`±${o.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Mr,{samples:je,emptyLabel:a("cam.focus.historyEmpty")}),Bt?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Bt}):null,o!=null&&o.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,o!=null&&o.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Ea).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((la=n==null?void 0:n.info)==null?void 0:la.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:Math.round(Number(i.actual_exposure_us??0)/1e3)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Ee.current!=null?`${Math.max(0,Math.round((performance.now()-Ee.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((da=n==null?void 0:n.info)==null?void 0:da.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ua=n==null?void 0:n.info)==null?void 0:ua.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((ma=n==null?void 0:n.info)==null?void 0:ma.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(xa=n==null?void 0:n.info)!=null&&xa.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((pa=n==null?void 0:n.info)==null?void 0:pa.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((ha=n==null?void 0:n.info)==null?void 0:ha.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((fa=n==null?void 0:n.info)==null?void 0:fa.width)??"—"}x${((ba=n==null?void 0:n.info)==null?void 0:ba.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(ga=n==null?void 0:n.info)!=null&&ga.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!Me,onClick:()=>void ot(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${Me?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Na,{className:"inline h-4 w-4"})," ",a(M?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!Qt,onClick:()=>void Aa(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${Qt?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Rr,{className:"inline h-4 w-4"})," ",a(M?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!sr,onClick:()=>void Oa(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(ja,{className:"inline h-4 w-4"})," ",a(ft?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!or,onClick:()=>void Da(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(wa,{className:"inline h-4 w-4"})," ",Le?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:qt,onClick:()=>void it(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Ma&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),C&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Nr,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:C.filename})]}),e.jsx("button",{type:"button",onClick:()=>ct(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(gr,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:ka(C.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:C.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(C.modified).toLocaleString()})]}),C.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsxs("span",{className:"font-mono",children:[C.exposure_us,"us"]})]}),C.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:C.analogue_gain})]}),C.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:C.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[ue.length===0&&!qt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),cr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Se===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[ka(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>Qa(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(wr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void Za(t.name),className:`rounded border px-2 py-1 ${Se===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Sr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void er(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(pr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:Oe<=1,onClick:()=>_e(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:Oe,total:Ae})}),e.jsx("button",{type:"button",disabled:Oe>=Ae,onClick:()=>_e(t=>Math.min(Ae,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:gt,onChange:t=>{vt(t.target.value),Q(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:yt,onChange:t=>{jt(t.target.value),Q(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:kt,onChange:t=>{St(t.target.value),Q(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Br.map(t=>e.jsx("button",{type:"button",onClick:()=>{Nt(t),Q(!0)},className:`rounded border px-2 py-1 ${wt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!ce,onClick:()=>void za(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Fr,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),ke&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Pe&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(O,{label:a("cam.controls.exposure"),value:u.exposure,min:100,max:12e4,step:100,unit:"us",disabled:Pe,onChange:t=>{w(!0),N(r=>({...r,exposure:t}))}}),e.jsx(O,{label:a("cam.controls.gain"),value:u.gain,min:1,max:24,step:.1,disabled:Pe,onChange:t=>{w(!0),N(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(O,{label:a("cam.controls.digitalGain"),value:u.digitalGain,min:1,max:8,step:.1,disabled:Pe||!Xt,onChange:t=>{w(!0),N(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:u.noiseReductionMode,onChange:t=>{w(!0),N(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:nr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(O,{label:a("cam.controls.contrast"),value:u.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(O,{label:a("cam.controls.brightness"),value:u.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),N(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(O,{label:a("cam.controls.saturation"),value:u.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(O,{label:a("cam.controls.sharpness"),value:u.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),!Xt&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((va=n==null?void 0:n.info)==null?void 0:va.actual_digital_gain)??u.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!ke,onClick:()=>void Ga(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:u.autoExposure?"auto":"manual",onChange:t=>{N(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:u.colorMode,onChange:t=>{N(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:u.whiteBalanceMode,onChange:t=>{N(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:rr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:u.aeFlickerMode,onChange:t=>{N(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:1e4,max:1e7,step:1e4,value:u.autoExposureMaxUs,onChange:t=>{N(r=>({...r,autoExposureMaxUs:S(parseInt(t.target.value,10)||2e6,1e4,1e7)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(O,{label:"R Gain",value:u.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!lt,onChange:t=>{N(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(O,{label:"B Gain",value:u.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!lt,onChange:t=>{N(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!lt&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void qa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(oe||ie)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[oe&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:oe}),ie&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:ie})]})]})}ur.createRoot(document.getElementById("root")).render(e.jsx(mr.StrictMode,{children:e.jsx(hr,{children:e.jsx(yr,{children:e.jsx(Tr,{})})})})); diff --git a/web/static/analysis-lab/assets/http-lbh6zBy9.js b/web/static/analysis-lab/assets/http-BxpNn71N.js similarity index 97% rename from web/static/analysis-lab/assets/http-lbh6zBy9.js rename to web/static/analysis-lab/assets/http-BxpNn71N.js index 7c4855d..3a6d3a1 100644 --- a/web/static/analysis-lab/assets/http-lbh6zBy9.js +++ b/web/static/analysis-lab/assets/http-BxpNn71N.js @@ -1,4 +1,4 @@ -import{c as u}from"./index-CV2LaaT8.js";import{r as o,j as f}from"./client-D1ZVDB-N.js";/** +import{c as u}from"./index-DfDaIsmY.js";import{r as o,j as f}from"./client-D1ZVDB-N.js";/** * @license lucide-react v0.460.0 - ISC * * This source code is licensed under the ISC license. diff --git a/web/static/analysis-lab/assets/index-BtAa6NQk.css b/web/static/analysis-lab/assets/index-BtAa6NQk.css deleted file mode 100644 index 79eb476..0000000 --- a/web/static/analysis-lab/assets/index-BtAa6NQk.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-3{left:.75rem}.left-\[10px\]{left:10px}.left-\[110px\]{left:110px}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-12{top:3rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-8{top:2rem}.top-\[18px\]{top:18px}.top-\[88px\]{top:88px}.top-\[94px\]{top:94px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.order-1{order:1}.order-3{order:3}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-0{height:0px}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100px\]{height:100px}.h-\[112px\]{height:112px}.h-\[152px\]{height:152px}.h-\[188px\]{height:188px}.h-\[460px\]{height:460px}.h-\[88px\]{height:88px}.h-\[calc\(100vh-7rem\)\]{height:calc(100vh - 7rem)}.h-dvh{height:100dvh}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[320px\]{max-height:320px}.max-h-\[420px\]{max-height:420px}.max-h-\[70vh\]{max-height:70vh}.max-h-\[min\(22rem\,55vh\)\]{max-height:min(22rem,55vh)}.max-h-\[min\(36vh\,18rem\)\]{max-height:min(36vh,18rem)}.max-h-\[min\(50vh\,28rem\)\]{max-height:min(50vh,28rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0px}.min-h-14{min-height:3.5rem}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[480px\]{min-height:480px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100px\]{width:100px}.w-\[200px\]{width:200px}.w-\[220px\]{width:220px}.w-\[360px\]{width:360px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[15rem\]{min-width:15rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[1880px\]{max-width:1880px}.max-w-\[220px\]{max-width:220px}.max-w-\[calc\(100\%-1\.5rem\)\]{max-width:calc(100% - 1.5rem)}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.border-separate{border-collapse:separate}.border-spacing-y-2{--tw-border-spacing-y: .5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-top-left{transform-origin:top left}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-px{--tw-translate-x: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-x-\[9px\]{border-left-width:9px;border-right-width:9px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-b-\[12px\]{border-bottom-width:12px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-2{border-right-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-amber-500\/40{border-color:#f59e0b66}.border-emerald-400\/40{border-color:#34d39966}.border-emerald-500\/20{border-color:#10b98133}.border-emerald-600\/35{border-color:#05966959}.border-error\/35{border-color:#ffb4ab59}.border-error\/40{border-color:#ffb4ab66}.border-error\/50{border-color:#ffb4ab80}.border-error\/60{border-color:#ffb4ab99}.border-outline-variant\/10{border-color:#4147541a}.border-outline-variant\/15{border-color:#41475426}.border-outline-variant\/20{border-color:#41475433}.border-outline-variant\/25{border-color:#41475440}.border-outline-variant\/30{border-color:#4147544d}.border-outline-variant\/40{border-color:#41475466}.border-primary{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.border-primary\/30{border-color:#adc6ff4d}.border-primary\/40{border-color:#adc6ff66}.border-primary\/50{border-color:#adc6ff80}.border-primary\/70{border-color:#adc6ffb3}.border-red-500\/40{border-color:#ef444466}.border-rose-400\/40{border-color:#fb718566}.border-sky-500\/30{border-color:#0ea5e94d}.border-teal-400\/75{border-color:#2dd4bfbf}.border-teal-500\/25{border-color:#14b8a640}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-white\/5{border-color:#ffffff0d}.border-x-transparent{border-left-color:transparent;border-right-color:transparent}.border-b-amber-400{--tw-border-opacity: 1;border-bottom-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-400\/95{background-color:#fbbf24f2}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-900\/55{background-color:#78350f8c}.bg-background{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-black\/25{background-color:#00000040}.bg-black\/35{background-color:#00000059}.bg-black\/40{background-color:#0006}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/80{background-color:#000c}.bg-emerald-800\/90{background-color:#065f46e6}.bg-error-container\/15{background-color:#93000a26}.bg-error-container\/20{background-color:#93000a33}.bg-error-container\/95{background-color:#93000af2}.bg-error\/10{background-color:#ffb4ab1a}.bg-error\/80{background-color:#ffb4abcc}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-neutral-950\/80{background-color:#0a0a0acc}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(173 198 255 / var(--tw-bg-opacity, 1))}.bg-primary-container{--tw-bg-opacity: 1;background-color:rgb(76 142 255 / var(--tw-bg-opacity, 1))}.bg-primary-container\/20{background-color:#4c8eff33}.bg-primary-container\/30{background-color:#4c8eff4d}.bg-primary-container\/50{background-color:#4c8eff80}.bg-primary\/10{background-color:#adc6ff1a}.bg-primary\/20{background-color:#adc6ff33}.bg-primary\/5{background-color:#adc6ff0d}.bg-red-500\/10{background-color:#ef44441a}.bg-secondary{--tw-bg-opacity: 1;background-color:rgb(64 229 108 / var(--tw-bg-opacity, 1))}.bg-secondary\/80{background-color:#40e56ccc}.bg-sky-300\/90{background-color:#7dd3fce6}.bg-sky-400{--tw-bg-opacity: 1;background-color:rgb(56 189 248 / var(--tw-bg-opacity, 1))}.bg-sky-400\/95{background-color:#38bdf8f2}.bg-sky-700\/80{background-color:#0369a1cc}.bg-sky-900\/50{background-color:#0c4a6e80}.bg-slate-500\/85{background-color:#64748bd9}.bg-slate-500\/90{background-color:#64748be6}.bg-slate-600\/40{background-color:#47556966}.bg-slate-600\/50{background-color:#47556980}.bg-slate-600\/60{background-color:#47556999}.bg-slate-900\/80{background-color:#0f172acc}.bg-slate-950\/50{background-color:#02061780}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-surface-container{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.bg-surface-container-high{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.bg-surface-container-high\/40{background-color:#272a3166}.bg-surface-container-high\/95{background-color:#272a31f2}.bg-surface-container-highest{--tw-bg-opacity: 1;background-color:rgb(50 53 60 / var(--tw-bg-opacity, 1))}.bg-surface-container-highest\/30{background-color:#32353c4d}.bg-surface-container-highest\/40{background-color:#32353c66}.bg-surface-container-highest\/95{background-color:#32353cf2}.bg-surface-container-low{--tw-bg-opacity: 1;background-color:rgb(25 28 34 / var(--tw-bg-opacity, 1))}.bg-surface-container-low\/80{background-color:#191c22cc}.bg-surface-container-low\/90{background-color:#191c22e6}.bg-surface-container-low\/95{background-color:#191c22f2}.bg-surface-container-lowest{--tw-bg-opacity: 1;background-color:rgb(11 14 20 / var(--tw-bg-opacity, 1))}.bg-surface-container-lowest\/90{background-color:#0b0e14e6}.bg-surface-container-lowest\/95{background-color:#0b0e14f2}.bg-surface-container\/50{background-color:#1d202680}.bg-surface-container\/60{background-color:#1d202699}.bg-surface-container\/80{background-color:#1d2026cc}.bg-surface-container\/90{background-color:#1d2026e6}.bg-violet-400{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity, 1))}.bg-violet-400\/95{background-color:#a78bfaf2}.bg-violet-900\/45{background-color:#4c1d9573}.bg-white\/5{background-color:#ffffff0d}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-300\/95{--tw-gradient-from: rgb(252 211 77 / .95) var(--tw-gradient-from-position);--tw-gradient-to: rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-emerald-600{--tw-gradient-from: #059669 var(--tw-gradient-from-position);--tw-gradient-to: rgb(5 150 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-600\/90{--tw-gradient-from: rgb(71 85 105 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/40{--tw-gradient-from: rgb(15 23 42 / .4) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/90{--tw-gradient-from: rgb(15 23 42 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-slate-800\/95{--tw-gradient-to: rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(30 41 59 / .95) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-slate-900\/70{--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(15 23 42 / .7) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-sky-400\/40{--tw-gradient-to: rgb(56 189 248 / .4) var(--tw-gradient-to-position)}.to-sky-500{--tw-gradient-to: #0ea5e9 var(--tw-gradient-to-position)}.to-slate-950{--tw-gradient-to: #020617 var(--tw-gradient-to-position)}.to-surface-container\/70{--tw-gradient-to: rgb(29 32 38 / .7) var(--tw-gradient-to-position)}.to-surface-container\/90{--tw-gradient-to: rgb(29 32 38 / .9) var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0\.5{padding-right:.125rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-body,.font-headline{font-family:system-ui,Segoe UI,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity, 1))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-200\/95{color:#fde68af2}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.text-on-primary-container{--tw-text-opacity: 1;color:rgb(0 40 93 / var(--tw-text-opacity, 1))}.text-on-surface{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.text-on-surface-variant{--tw-text-opacity: 1;color:rgb(194 198 214 / var(--tw-text-opacity, 1))}.text-on-surface-variant\/70{color:#c2c6d6b3}.text-on-surface-variant\/80{color:#c2c6d6cc}.text-on-surface-variant\/85{color:#c2c6d6d9}.text-on-surface-variant\/90{color:#c2c6d6e6}.text-on-surface-variant\/95{color:#c2c6d6f2}.text-on-surface\/80{color:#e1e2ebcc}.text-primary{--tw-text-opacity: 1;color:rgb(173 198 255 / var(--tw-text-opacity, 1))}.text-primary\/80{color:#adc6ffcc}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(64 229 108 / var(--tw-text-opacity, 1))}.text-sky-100{--tw-text-opacity: 1;color:rgb(224 242 254 / var(--tw-text-opacity, 1))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.text-sky-300\/90{color:#7dd3fce6}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-teal-100{--tw-text-opacity: 1;color:rgb(204 251 241 / var(--tw-text-opacity, 1))}.text-teal-200{--tw-text-opacity: 1;color:rgb(153 246 228 / var(--tw-text-opacity, 1))}.text-teal-200\/95{color:#99f6e4f2}.text-violet-200{--tw-text-opacity: 1;color:rgb(221 214 254 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-primary{accent-color:#adc6ff}.opacity-50{opacity:.5}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(255\,255\,255\,0\.6\)\]{--tw-shadow: 0 0 6px rgba(255,255,255,.6);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_20px_50px_rgba\(0\,0\,0\,0\.55\)\,inset_0_1px_0_rgba\(255\,255\,255\,0\.08\)\]{--tw-shadow: 0 20px 50px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);--tw-shadow-colored: 0 20px 50px var(--tw-shadow-color), inset 0 1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/35{--tw-ring-color: rgb(253 230 138 / .35)}.ring-sky-200\/30{--tw-ring-color: rgb(186 230 253 / .3)}.ring-sky-500\/25{--tw-ring-color: rgb(14 165 233 / .25)}.ring-slate-600\/50{--tw-ring-color: rgb(71 85 105 / .5)}.ring-teal-500\/10{--tw-ring-color: rgb(20 184 166 / .1)}.ring-violet-200\/35{--tw-ring-color: rgb(221 214 254 / .35)}.ring-white\/5{--tw-ring-color: rgb(255 255 255 / .05)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}html,body,#root{height:100%}.og-scrollbar{scrollbar-width:thin;scrollbar-color:#414754 #191c22}.og-scrollbar::-webkit-scrollbar{width:7px;height:7px}.og-scrollbar::-webkit-scrollbar-track{background:#191c22;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb{background:#414754;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb:hover{background:#5c6070}.hover\:border-primary:hover{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.hover\:bg-amber-500\/10:hover{background-color:#f59e0b1a}.hover\:bg-emerald-500\/10:hover{background-color:#10b9811a}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity, 1))}.hover\:bg-error-container\/20:hover{background-color:#93000a33}.hover\:bg-error-container\/30:hover{background-color:#93000a4d}.hover\:bg-error\/10:hover{background-color:#ffb4ab1a}.hover\:bg-rose-500\/10:hover{background-color:#f43f5e1a}.hover\:bg-sky-600:hover{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container:hover{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container-high:hover{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container\/80:hover{background-color:#1d2026cc}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-error:hover{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.hover\:text-on-surface:hover{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-4{gap:1rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:min-w-\[200px\]{min-width:200px}.md\:max-w-md{max-width:28rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-center{justify-content:center}.md\:gap-8{gap:2rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:ml-64{margin-left:16rem}.lg\:hidden{display:none}.lg\:max-w-lg{max-width:32rem}.lg\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:p-6{padding:1.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:max-w-\[340px\]{max-width:340px}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:gap-8{gap:2rem}.xl\:self-start{align-self:flex-start}}@media (min-width: 1536px){.\32xl\:static{position:static}.\32xl\:order-1{order:1}.\32xl\:order-2{order:2}.\32xl\:col-span-10{grid-column:span 10 / span 10}.\32xl\:col-span-2{grid-column:span 2 / span 2}.\32xl\:col-span-3{grid-column:span 3 / span 3}.\32xl\:col-span-9{grid-column:span 9 / span 9}.\32xl\:hidden{display:none}.\32xl\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\32xl\:self-start{align-self:flex-start}.\32xl\:shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none} diff --git a/web/static/analysis-lab/assets/index-CV2LaaT8.js b/web/static/analysis-lab/assets/index-CV2LaaT8.js deleted file mode 100644 index 75a83a2..0000000 --- a/web/static/analysis-lab/assets/index-CV2LaaT8.js +++ /dev/null @@ -1,26 +0,0 @@ -import{r as a,j as v}from"./client-D1ZVDB-N.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 f=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),u=(...e)=>e.filter((s,t,o)=>!!s&&s.trim()!==""&&o.indexOf(s)===t).join(" ").trim();/** - * @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. - */var w={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** - * @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 S=a.forwardRef(({color:e="currentColor",size:s=24,strokeWidth:t=2,absoluteStrokeWidth:o,className:l="",children:r,iconNode:m,...n},c)=>a.createElement("svg",{ref:c,...w,width:s,height:s,stroke:e,strokeWidth:o?Number(t)*24/Number(s):t,className:u("lucide",l),...n},[...m.map(([i,d])=>a.createElement(i,d)),...Array.isArray(r)?r:[r]]));/** - * @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 P=(e,s)=>{const t=a.forwardRef(({className:o,...l},r)=>a.createElement(S,{ref:r,iconNode:s,className:u(`lucide-${f(e)}`,o),...l}));return t.displayName=`${e}`,t};/** - * @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 A=P("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]),k={"app.title":"OGScope Plate Solve Console","nav.lab":"Lab","nav.labImage":"Image solve","nav.labVideo":"Video solve","delete.uploadCascade":"Delete {n} linked experiment record(s) as well?","lab.solveCurrentFrame":"Solve current frame (file)","lab.solveFileStart":"Start continuous file solve","lab.solveFileStop":"Stop continuous file solve","lab.cameraPreviewLoading":"Connecting to shared preview…","lab.solveCameraFrame":"Solve live camera frame","lab.solveCameraStart":"Start camera solve","lab.solveCameraStop":"Stop camera solve","lab.videoPreviewFailed":"This video format may be unsupported by the browser. Try MP4 (H.264) or WebM.","lab.previewModeFile":"Pool file","lab.previewModeCamera":"Device camera","lab.videoLiveIntro":"Shares the same camera as Camera Debug — preview and solve live frames here without opening debug. Both pages can run together.","lab.videoMjpegHint":"MJPEG slots are full: close live previews on other pages or retry later (default ~4; tune OGSCOPE_STREAM_MAX_MJPEG_CLIENTS in systemd).","lab.cameraStreamStatusFail":"Could not read MJPEG slot status. Please retry shortly.","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"Raw Prob","lab.systemLoad":"System load","results.saveBatchAll":"Save all to records","nav.pool":"Assets","nav.history":"Records","nav.cameraDebug":"Camera Debug","nav.home":"Home","nav.systemAdmin":"Back to System Admin","lang.zh":"中文","lang.en":"EN","sidebar.assets":"Uploaded assets","sidebar.upload":"Upload","sidebar.refresh":"Refresh","sidebar.debugCaptures":"Debug console media","sidebar.assetTypeImage":"Image","sidebar.assetTypeVideo":"Video","sidebar.debugEmpty":"No debug files","sidebar.importToPool":"Import to pool","sidebar.importToPoolWithTranscode":"Transcode & Import","sidebar.importToPoolDirect":"Import Directly","sidebar.flowPreparing":"Preparing...","sidebar.flowImportingDebug":"Importing debug capture...","sidebar.flowDownloadingPool":"Downloading imported file...","sidebar.flowWritingBuffer":"Writing to transcoder buffer...","sidebar.flowLoadingTranscoder":"Loading transcoder...","sidebar.flowTranscoding":"Transcoding AVI -> MP4...","sidebar.flowPackaging":"Packaging output...","sidebar.flowUploadingMp4":"Uploading MP4...","sidebar.flowReplacing":"Replacing and removing original AVI...","sidebar.flowNoTranscode":"No transcode required, imported directly.","sidebar.flowDone":"Completed","sidebar.flowDoneMsg":"Done: {name}","sidebar.flowFailed":"Failed","sidebar.debugPage":"Page {cur} / {total}","sidebar.batchPresets":"Batch presets","sidebar.batchHint":"Check presets, then use Batch solve to compare multiple param sets.","lab.selectOrUpload":"Pick an uploaded or imported asset from the left","lab.selectOrUploadVideo":"Pick a pool video to preview, or use the button above for the live camera frame.","lab.file":"File","lab.source":"Source","lab.layers":"Layers","lab.layer.matched":"Matched","lab.layer.pattern":"Pattern","lab.layer.all":"All centroids","lab.layer.rejected":"Rejected (red cross)","lab.previewConfidence":"Confidence","lab.grid":"Grid","lab.zoomIn":"Zoom in","lab.zoomOut":"Zoom out","lab.zoomReset":"Reset","lab.resolution":"Resolution","lab.fwhm":"FWHM","lab.starsDetected":"Stars detected","lab.meta.title":"Capture & file info","lab.meta.noSidecar":"No sidecar (not from debug capture)","lab.meta.partial":"No detailed sidecar; file info only.","lab.solveSection":"Solve","lab.imageSection":"Image","lab.metric.solveMs":"Time","lab.metric.solveComputeMs":"Solve compute","lab.metric.solveComputeHelp":"Server-side Tetra3 + star extraction only (no network).","lab.metric.solveRoundTripMs":"End-to-end","lab.metric.solveRoundTripHelp":"From request start to UI updated: network + JSON + render.","lab.metric.backendTotalMs":"Backend total","lab.metric.openDecodeMs":"Open/decode","lab.metric.preprocessMs":"Preprocess","lab.metric.extractMs":"Extract","lab.metric.solveOnlyMs":"Solve match","lab.metric.probHelp":"Tetra3 `Prob` is false-positive probability (lower is better). We use (1−Prob)×100% when representable; for extremely small Prob, IEEE doubles round to 100%, so we map −log10(Prob) to ~70–100% for differentiation.","lab.metric.probRawHelp":"Raw Tetra3 Prob (e.g. log-likelihood); compare with the normalized line above.","lab.metric.radec":"RA / Dec","lab.metric.matches":"Matches","lab.metric.rmse":"RMSE","lab.metric.prob":"Prob.","lab.metric.status":"Status","lab.gateNextMs":"Retry in ~{ms} ms","lab.gate.skipRecording":"Recording is active: the camera is writing video, so live-frame solve is paused. Stop recording and try again.","lab.gate.skipBusyInFlight":"The previous solve is still running; please wait. If this repeats often, increase the realtime solve interval.","lab.gate.skipInterval":"Solve requests are arriving faster than allowed; the server is throttling. Wait for the countdown and retry.","lab.gate.skipBusyFallback":"Realtime solve could not start ({detail}).","lab.centroidQualityTitle":"Centroid quality","lab.centroidQualityMetrics":"In {in} → kept {out}; removed dense {dense}, collinear {line}","meta.exposure":"Exposure","meta.gain":"Gain","meta.fps":"FPS","meta.sensor":"Sensor","meta.colorMode":"Color","meta.outputResolution":"Output size","meta.fileTime":"File time","meta.fileSize":"File size","results.viewRaw":"Raw JSON","results.hideRaw":"Hide","results.solveHistoryTitle":"Solve history","results.historySingle":"Single frame","results.historyBatch":"Batch ({count} runs)","params.title":"Solve parameters","params.blockSolveIntro":"Plate-solve (Tetra3): FOV, timeout, and coarse sky hints. FOV should match your lens.","params.centroid":"Star detection","params.blockCentroidIntro":"Star detection: threshold, blob area, and local background window for centroids.","params.fov":"FOV estimate (°)","params.fovHelp":"Horizontal field of view for lost-in-space solve.","params.fovErr":"FOV max error (°)","params.fovErrHelp":"Search range around estimated FOV.","params.timeout":"Timeout (ms)","params.timeoutHelp":"Max wait time per solve.","params.solveIntervalMs":"Realtime solve interval (ms)","params.solveIntervalMsHelp":"Desired interval is adjustable, but backend clamps it into a safe range.","params.solveIntervalIndependent":"Solve cadence is independent from sensor and preview frame rates.","params.solveIntervalBound":"Backend bounds: {min}-{max} ms, effective now: {effective} ms","params.centroidRejectionLevel":"Centroid rejection (1–5)","params.centroidRejectionLevelHelp":"1 keeps more stars; 5 aggressively rejects dense clusters and collinear false detections; default 3.","params.centroidRejectionScale":"Conservative ← → Aggressive","params.solveProfile":"Solve profile","params.solveProfileHelp":"Speed/Balanced/Robust tune timeout, centroid thresholds, and matching star count together.","params.solveProfileSpeed":"Speed first","params.solveProfileBalanced":"Balanced","params.solveProfileRobust":"Robust first","params.ra":"RA hint (°)","params.raHelp":"Approximate right ascension in degrees.","params.dec":"Dec hint (°)","params.decHelp":"Approximate declination in degrees.","params.maxSide":"Max long side before extract (px)","params.maxSideHelp":"Downscale long edge for faster centroid extraction.","params.detailLevelFull":"Include full Tetra3 raw block (larger payload, for debugging only).","params.largeScaleBg":"Large-scale background flattening","params.largeScaleBgHelp":"Before centroiding, estimate a low-frequency background on a downscaled image and correct uneven illumination (e.g. corner glow). Off by default to match legacy behavior.","params.sigma":"σ threshold","params.sigmaHelp":"Multiplier over background noise for star candidates.","params.maxArea":"max_area","params.maxAreaHelp":"Max connected component area in pixels.","params.minArea":"min_area","params.minAreaHelp":"Min connected component area in pixels.","params.filtsize":"filtsize (odd)","params.filtsizeHelp":"Local filter window size, must be odd.","btn.solveOne":"Solve once","btn.solveBatch":"Batch solve (presets)","btn.applyPresets":"Apply preset to form","btn.savePreset":"Save","placeholder.newPreset":"New preset name","pool.title":"Server asset pool","pool.col.name":"Filename","pool.col.source":"Source","pool.col.size":"Size","pool.col.time":"Modified","pool.delete":"Delete","history.title":"Experiment records","history.intro":"Saved solve snapshots from the Lab. After a solve, use Save to records in the Lab main panel (Result comparison), or Save on each batch result card. Search by filename or preset; export JSON/CSV for backup.","history.search":"Search…","history.searchBtn":"Search","history.exportJson":"Export JSON","history.exportCsv":"Export CSV","history.total":"Total {n}","history.preset":"Preset","history.metrics":"Metrics","history.detail":"Details","history.collapse":"Collapse","history.prev":"Prev","history.next":"Next","history.delete":"Delete","delete.uploadFirst":'Delete "{name}" from the asset pool?',"delete.uploadSecond":"This cannot be undone. Confirm again?","delete.experimentFirst":"Delete this experiment record?","delete.experimentSecond":"This cannot be undone. Confirm again?","results.title":"Results","results.saveCurrent":"Save to records","results.saveRow":"Save","results.expand":"Expand","results.collapseJson":"Collapse","err.selectFile":"Select a file","err.selectPresets":"Select at least one preset","common.placeholder":"—","lab.transcode.title":"AVI requires transcoding","lab.transcode.desc":"This video is AVI. For browser preview and continuous solving, transcode it locally to MP4 and upload replacement. The original AVI on server will be removed after success.","lab.transcode.button":"Transcode and Upload Replacement","lab.transcode.loading":"Loading transcoder...","lab.transcode.writingBuffer":"Writing to transcoder buffer...","lab.transcode.running":"Transcoding AVI -> MP4...","lab.transcode.packaging":"Packaging transcoded output...","lab.transcode.uploading":"Uploading transcoded output...","lab.transcode.done":"Transcode and replacement completed.","lab.transcode.failed":"Transcode or upload failed. Please retry.","lab.transcode.fetchFailed":"Failed to fetch AVI from server.","sys.shell.subtitle":"system debug console","sys.shell.nav.overview":"Overview","sys.shell.nav.network":"Network","sys.shell.nav.camera":"Camera Debug","sys.shell.nav.analysis":"Analysis Console","sys.shell.nav.sensors":"Sensors","sys.shell.nav.power":"Power","sys.shell.nav.hmi":"HMI","sys.shell.nav.config":"Config","sys.shell.workbench":"WORKBENCH / System workspace","sys.shell.node":"Node","sys.shell.top.overview":"System Status","sys.shell.top.network":"Network & WiFi","sys.shell.top.sensors":"Sensors","sys.shell.top.power":"Power Management","sys.shell.top.hmi":"HMI","sys.shell.top.config":"Config","sys.overview.breadcrumb.console":"Console","sys.overview.breadcrumb.module":"System Overview","sys.overview.title":"System Overview","sys.overview.subtitle":"Live system telemetry snapshot","sys.overview.metric.cpu":"CPU Usage","sys.overview.metric.mem":"Memory Usage","sys.overview.metric.temp":"Core Temperature","sys.overview.metric.wifi":"WiFi Link","sys.overview.metric.uptime":"Uptime","sys.overview.metric.load":"Load (1m)","sys.overview.metric.storage":"Storage","sys.overview.linkActive":"Link Active","sys.overview.tempState":"status: nominal","sys.overview.wifiSummary":"WiFi Summary","sys.overview.iface":"Interface","sys.overview.signal":"Signal","sys.overview.quality":"Quality","sys.overview.storageComingSoon":"Coming soon","sys.logs.title":"System Logs","sys.logs.kernel":"Kernel","sys.logs.liveToggle":"Live pull","sys.logs.refresh":"Refresh","sys.logs.liveOffHint":"Live pull is off (default)","sys.logs.loading":"Loading logs...","sys.logs.empty":"No logs","sys.placeholder.breadcrumb":"Console / Reserved Module","sys.placeholder.block":"PLANNED BLOCK","sys.placeholder.desc":"Shell layout is reserved and ready for real data integration.","sys.placeholder.status":"STATUS: RESERVED / Structured placeholder page.","sys.placeholder.sensors.title":"Sensors Console","sys.placeholder.sensors.desc":"Unified monitor for IMU, environment, power rails, and timeline sampling.","sys.placeholder.sensors.block1":"Realtime chart area","sys.placeholder.sensors.block2":"Device health cards","sys.placeholder.sensors.block3":"Threshold alerts","sys.sensors.title":"Sensors Console","sys.sensors.desc":"Visual compass (magnetometer) and gyro rate gauges; backed by dev debug APIs.","sys.sensors.mag.section":"Magnetometer (AK09911 / I²C)","sys.sensors.mag.note":"Defaults: bus 1, address 12 (0x0C with CAD to GND). No app-level GPIO mapping is required; enable I²C in firmware and load i2c-dev.","sys.sensors.mag.bus":"I²C bus","sys.sensors.mag.addr":"7-bit address (decimal, 12 = 0x0C)","sys.sensors.mag.i2cdetect":"Run i2cdetect","sys.sensors.mag.btnSelftest":"Run self-test","sys.sensors.mag.btnProbe":"Probe all buses","sys.sensors.mag.btnCalStart":"Start heading calibration","sys.sensors.mag.btnCalCommit":"Save & lock heading","sys.sensors.mag.btnCalReset":"Reset to auto mode","sys.sensors.mag.btnCalStatus":"Calibration status","sys.sensors.mag.running":"Running…","sys.sensors.mpu.section":"IMU (MPU-6050 / I²C)","sys.sensors.mpu.note":"Defaults: bus 1, address 104 (0x68 with AD0 low). Includes gyro angular-rate sampling (MPU integrates accel + gyro).","sys.sensors.mpu.addr":"7-bit address (decimal, 104 = 0x68)","sys.sensors.mpu.btnSelftest":"Run MPU self-test","sys.sensors.mpu.running":"Running…","sys.sensors.gyro.title":"Gyroscope (MPU-6050)","sys.sensors.gyro.subtitle":"Uses the same bus/address as above; one IMU read: 3-axis gyro, accel-derived roll/pitch, and a 3D board hint.","sys.sensors.gyro.btn":"Read angular rate","sys.sensors.gyro.loading":"Sampling…","sys.sensors.gyro.dps":"Angular rate","sys.sensors.gyro.raw":"Raw","sys.sensors.gyro.unitDps":"°/s, ±250 °/s range, default sensitivity","sys.sensors.gyro.unitRaw":"signed 16-bit","sys.sensors.gyro.hint":"Click “Read angular rate” to sample accel (0x3B) and gyro (0x43). Roll/pitch use gravity; ωz is yaw rate, not absolute heading.","sys.sensors.gyro.errUnknown":"Sample failed","sys.sensors.gyro.live":"Live update (~2 Hz)","sys.sensors.gyro.barsTitle":"Angular rate bars (±250 °/s full scale)","sys.sensors.gyro.rawBlock":"Raw register values (16-bit)","sys.sensors.gyro.att3dTitle":"3D attitude hint","sys.sensors.gyro.att3dDesc":"Roll/pitch from accelerometer (gravity); ωz is yaw rate (not magnetic heading). Body XYZ triad is fixed to the PCB and rotates in 3D.","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"Body frame on the PCB: +X amber, +Y sky, +Z violet (normal); short dashes mark negative directions; the whole frame follows roll/pitch.","sys.sensors.gyro.att3dRefTitle":"Orthogonal axes (fixed view)","sys.sensors.gyro.att3dRefDesc":"Same colors as left; use this to read XYZ directions at a glance.","sys.sensors.gyro.roll":"Roll","sys.sensors.gyro.pitch":"Pitch","sys.sensors.gyro.yawRate":"Yaw rate","sys.sensors.compass.tapeCaption":"Heading tape · center = current readout","sys.sensors.compass.title":"Magnetometer · North compass","sys.sensors.compass.desc":"Horizontal-plane heading for “roughly north” debugging; precise alignment needs calibration and a fixed mount definition.","sys.sensors.compass.btn":"Refresh compass","sys.sensors.compass.loading":"Measuring…","sys.sensors.compass.live":"Live update","sys.sensors.compass.headingLabel":"Horizontal heading","sys.sensors.compass.headingHint":"From atan2(Hx, Hy), degrees","sys.sensors.compass.cardN":"N","sys.sensors.compass.cardE":"E","sys.sensors.compass.cardS":"S","sys.sensors.compass.cardW":"W","sys.sensors.compass.err":"Sample failed","sys.sensors.compass.footnote":"The red tip tracks the horizontal XY field direction; it should move as you rotate the board. Use soft/hard-iron calibration for tight north lock.","sys.sensors.jsonToggle":"Raw JSON (troubleshooting)","sys.hmi.title":"HMI · SPI display","sys.hmi.desc":"ST7796 via hardware plane (default 320×320). Requires OGSCOPE_DISPLAY_ENABLED=true and SPI enabled (/dev/spidev0.0).","sys.hmi.status.section":"Display & hardware plane","sys.hmi.status.refresh":"Refresh","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"present","sys.hmi.status.no":"missing","sys.hmi.status.resolution":"Resolution / DC","sys.hmi.status.driver":"Driver handle","sys.hmi.status.open":"open","sys.hmi.status.closed":"closed","sys.hmi.status.screenOutput":"Logical output","sys.hmi.status.on":"on","sys.hmi.status.off":"off","sys.hmi.status.lastPattern":"Last pattern","sys.hmi.status.lastError":"Last error","sys.hmi.actions.section":"Output to panel","sys.hmi.actions.hint":"Uses hardware plane device.command → hmi; large frames use a longer RPC timeout.","sys.hmi.actions.smoke":"Test pattern (text + frame)","sys.hmi.actions.colorbars":"Color bars","sys.hmi.actions.fill":"Fill RGB","sys.hmi.actions.screenOn":"Enable panel output","sys.hmi.actions.screenOff":"Pause panel output","sys.hmi.actions.release":"Release driver (close SPI/GPIO)","sys.hmi.rawJson":"Raw JSON response","sys.placeholder.hmi.title":"HMI Console","sys.placeholder.hmi.desc":"Debug workspace for SPI display, key matrix, backlight, and UI replay.","sys.placeholder.hmi.block1":"Display preview","sys.placeholder.hmi.block2":"Input event stream","sys.placeholder.hmi.block3":"Mode controls","sys.placeholder.power.title":"Power Console","sys.placeholder.power.desc":"Debug workspace for battery, charging, power profile, and thermal policy.","sys.placeholder.power.block1":"Power dashboard","sys.placeholder.power.block2":"Power policy table","sys.placeholder.power.block3":"Recovery actions","cam.title":"Camera Debug Console","cam.subtitle":"Migrated to unified admin UI with key optimizations preserved","cam.btn.system":"System Admin","cam.btn.analysis":"Analysis Console","cam.btn.home":"Home","cam.btn.start":"Start Preview","cam.btn.starting":"Starting...","cam.btn.stop":"Stop Preview","cam.btn.stopping":"Stopping...","cam.btn.capture":"Capture","cam.btn.capturing":"Capturing...","cam.btn.recordStart":"Start Recording","cam.btn.recordStop":"Stop Recording","cam.btn.recordBusy":"Switching recording...","cam.btn.refresh":"Refresh Status","cam.preview.title":"Live Preview","cam.preview.state":"State","cam.preview.mode":"Exposure Mode","cam.preview.emptyTitle":"Preview is not started","cam.preview.emptyDesc":"Start the camera preview stream with the button below.","cam.state.streaming":"streaming","cam.state.idle":"idle","cam.state.rec":"REC","cam.stats.requestFps":"Request FPS","cam.hint.mjpegSingleStream":"MJPEG concurrent streams are limited (default ~2). Beyond the cap, extra tabs may contend or get busy—reduce simultaneous live previews.","cam.stats.frameFps":"Display FPS","cam.stats.fpsMeasureNote":"Pixel deltas on a 32×32 sample over ~1s. Multiple previews, slow links, or a stream cap below the sensor target often read much lower.","cam.err.streamBusy":"Preview unavailable: MJPEG slot is already in use","cam.err.streamBusyHint":"Close the live preview in other tabs or in the Analysis console, then try again.","cam.err.streamProbeFailed":"Could not open the preview stream","cam.err.streamProbeDetail":"Check network and camera service, or retry in a moment.","cam.stats.targetFps":"Camera capture target FPS","cam.stats.streamPacingFps":"Shared stream / MJPEG cap FPS","cam.stats.streamPacingHint":"Matches OGSCOPE_SHARED_PREVIEW_FPS: shared grabber pacing and per-stream MJPEG min frame spacing.","cam.stats.captureFps":"Actual capture FPS","cam.stats.encodeMs":"Average JPEG encode","cam.stats.encoder":"Preview encoder","cam.stats.driver":"Camera driver","cam.stats.metadata":"Metadata","cam.stats.consumers":"Preview/analysis/record consumers","cam.stats.cameraMemory":"Process memory/swap","cam.stats.longExposureThrottle":"Auto exposure is about {exposure} ms, so long exposure is limiting FPS; exposure is not shortened to preserve analysis quality.","cam.stats.uptime":"Stream Uptime","cam.controls.title":"Controls","cam.controls.core":"Core Settings","cam.controls.fps":"FPS","cam.controls.sensorFps":"Sensor target FPS","cam.controls.previewFps":"Preview target FPS","cam.controls.resolution":"Resolution","cam.controls.applyRes":"Apply resolution","cam.controls.sampling":"Sampling mode","cam.controls.exposure":"Exposure","cam.controls.gain":"Analog Gain","cam.controls.digitalGain":"Digital Gain","cam.controls.noiseReduction":"Noise Reduction","cam.controls.noiseReductionMode":"Noise Reduction","cam.controls.digitalGainReadOnly":"Digital gain is read-only on this driver","cam.controls.aeFlicker":"AE Flicker","cam.controls.maxAeFrame":"Max AE frame (us)","cam.controls.contrast":"Contrast","cam.controls.brightness":"Brightness","cam.controls.saturation":"Saturation","cam.controls.sharpness":"Sharpness","cam.controls.applySettings":"Apply All Settings","cam.controls.applyAll":"Save Parameters","cam.controls.applyRuntime":"Save Runtime Settings","cam.controls.mode":"Mode Controls","cam.controls.autoExposure":"Exposure Mode","cam.controls.whiteBalance":"White Balance","cam.controls.colorMode":"Color Mode","cam.controls.auto":"Auto","cam.controls.manual":"Manual","cam.controls.night":"Night","cam.controls.off":"Off","cam.controls.nr.off":"Off","cam.controls.nr.fast":"Fast","cam.controls.nr.high_quality":"High quality","cam.controls.wb.auto":"Auto","cam.controls.wb.daylight":"Daylight","cam.controls.wb.cloudy":"Cloudy","cam.controls.wb.tungsten":"Tungsten","cam.controls.wb.fluorescent":"Fluorescent","cam.controls.wb.indoor":"Indoor","cam.controls.wb.manual":"Manual","cam.controls.wb.night":"Night","cam.controls.color":"Color","cam.controls.mono":"Mono","cam.controls.applyAe":"Apply Exposure Mode","cam.controls.applyWb":"Apply White Balance","cam.controls.applyColor":"Apply Color Mode","cam.controls.applyMode":"Apply Mode Settings","cam.controls.lockedByAe":"Exposure and gain are locked while auto exposure is enabled.","cam.controls.lockedByWb":"R/B gains are locked unless white balance is in manual mode.","cam.controls.pendingChanges":"You have unapplied parameter changes","cam.controls.tools":"Tools","cam.controls.nightPreset":"Night Preset","cam.controls.nightOn":"Enable Night Mode","cam.controls.nightOff":"Disable Night Mode","cam.controls.backup":"Backup Settings","cam.controls.restore":"Restore Settings","cam.controls.reset":"Reset Camera","cam.quick.title":"Quick Presets & Smart Tuning","cam.quick.daylight":"Daylight","cam.quick.night":"Night","cam.quick.nightPreset":"Apply Night Preset","cam.quick.deep-sky":"Deep Sky","cam.quick.planetary":"Planetary","cam.quick.autoAdjust":"Auto Adjust","cam.quick.nightHint":"Night preset is one-click tuning; night mode toggle is runtime on/off control.","cam.system.title":"System Monitor","cam.system.status":"Camera Ready","cam.system.stream":"Stream","cam.system.sensor":"Sensor","cam.system.quality":"Image Quality","cam.presets.title":"Preset Management","cam.presets.name":"Preset Name","cam.presets.desc":"Preset Description","cam.presets.save":"Save Preset","cam.presets.empty":"No presets","cam.presets.noDesc":"No description","cam.presets.apply":"Apply","cam.presets.delete":"Delete","cam.files.title":"File Management","cam.files.refresh":"Refresh File List","cam.files.empty":"No files","cam.files.download":"Download","cam.files.info":"Details","cam.files.delete":"Delete","cam.files.loadingInfo":"Loading file details...","cam.files.prev":"Prev","cam.files.next":"Next","cam.files.page":"Page {current}/{total}","cam.files.closeDetail":"Close details","cam.files.size":"Size","cam.files.type":"Type","cam.files.modified":"Modified","cam.hist.title":"Histogram","cam.hist.enabled":"Enable histogram","cam.hist.luminance":"Luminance","cam.hist.over":"Over-exposure","cam.hist.expand":"Expand Histogram","cam.hist.collapse":"Collapse Histogram","cam.notice.previewStart":"Preview started","cam.notice.previewStop":"Preview stopped","cam.notice.captureSaved":"Capture saved: {name}","cam.notice.recordStart":"Recording started: {name}","cam.notice.recordStop":"Recording stopped","cam.notice.fpsApplied":"FPS updated to {fps}","cam.notice.resApplied":"Resolution updated to {w}x{h}","cam.notice.samplingApplied":"Sampling mode updated: {mode}","cam.notice.settingsApplied":"Settings applied","cam.notice.aeApplied":"Exposure mode applied","cam.notice.wbApplied":"White balance applied","cam.notice.colorApplied":"Color mode applied","cam.notice.modeApplied":"Mode settings applied","cam.notice.runtimeApplied":"Runtime settings applied","cam.notice.rotationApplied":"Rotation set to {value}°","cam.mirror.hint":"Use with rotation when the camera is mounted inverted; preview, plate solve, and polar guide share one coordinate frame.","cam.mirror.horizontal":"Horizontal mirror","cam.mirror.vertical":"Vertical mirror","cam.notice.mirrorApplied":"Mirror settings updated","cam.notice.nightOn":"Night mode enabled","cam.notice.nightOff":"Night mode disabled","cam.notice.nightPreset":"Night preset applied","cam.notice.reset":"Camera settings reset","cam.notice.backup":"Settings backed up","cam.notice.restore":"Settings restored","cam.notice.presetSaved":"Preset saved: {name}","cam.notice.presetApplied":"Preset applied: {name}","cam.notice.presetDeleted":"Preset deleted: {name}","cam.notice.download":"Download started: {name}","cam.notice.downloadWithSidecar":"Downloaded: {name} (with sidecar {sidecar})","cam.notice.fileDeleted":"File deleted: {name}","cam.notice.quickPresetApplied":"Quick preset applied: {preset}","cam.notice.needPreview":"Start preview first","cam.notice.needManualForAutoAdjust":"Switch to manual exposure before auto-adjust","cam.notice.autoAdjusted":"Auto-adjust completed and applied","cam.confirm.reset":"Reset camera settings on the current device?","cam.confirm.deletePreset":"Delete preset {name}?","cam.confirm.deleteFile":"Delete file {name}?","sys.sensors.mag.calStatusPrefix":"Calibration mode:","sys.sensors.mag.calModeAuto":"Auto (not locked)","sys.sensors.mag.calModeRecording":"Recording (rotate device)","sys.sensors.mag.calModeLocked":"Locked (saved heading)","sys.sensors.mag.calSamplesPrefix":"Recorded samples:","sys.sensors.mag.calRecordingHint":"Tip: keep level and rotate slowly; save after 10+ samples.","sys.sensors.mag.calLockedHint":"Locked heading params:"},C={"app.title":"OGScope 星空解算控制台","nav.lab":"解算台","nav.labImage":"图片解算","nav.labVideo":"视频解算","nav.pool":"素材池","nav.history":"实验记录","nav.cameraDebug":"相机调试控制台","nav.home":"首页","nav.systemAdmin":"返回系统后台","lang.zh":"中文","lang.en":"EN","sidebar.assets":"自行上传素材","sidebar.upload":"上传文件","sidebar.refresh":"刷新列表","sidebar.debugCaptures":"调试控制台素材","sidebar.assetTypeImage":"图片","sidebar.assetTypeVideo":"视频","sidebar.debugEmpty":"暂无调试文件","sidebar.importToPool":"导入到素材池","sidebar.importToPoolWithTranscode":"转码并导入素材池","sidebar.importToPoolDirect":"直接导入素材池","sidebar.flowPreparing":"准备中…","sidebar.flowImportingDebug":"正在导入调试素材…","sidebar.flowDownloadingPool":"正在下载已导入文件…","sidebar.flowWritingBuffer":"正在写入转码缓冲区…","sidebar.flowLoadingTranscoder":"正在加载转码器…","sidebar.flowTranscoding":"正在转码 AVI -> MP4…","sidebar.flowPackaging":"正在封装转码结果…","sidebar.flowUploadingMp4":"正在上传 MP4…","sidebar.flowReplacing":"正在替换并清理原 AVI…","sidebar.flowNoTranscode":"该文件无需转码,已直接导入。","sidebar.flowDone":"流程完成","sidebar.flowDoneMsg":"已完成:{name}","sidebar.flowFailed":"流程失败","sidebar.debugPage":"第 {cur} / {total} 页","sidebar.batchPresets":"批量预设","sidebar.batchHint":"勾选后点击「批量解算」可一次用多组参数对比结果。","lab.selectOrUpload":"从左侧选择自行上传或已导入的素材","lab.selectOrUploadVideo":"从左侧选择视频素材预览;或使用上方按钮解算设备相机实时帧。","lab.file":"文件","lab.source":"来源","lab.layers":"叠加层","lab.layer.matched":"匹配星","lab.layer.pattern":"图案星","lab.layer.all":"全部质心","lab.layer.rejected":"已剔除(红叉)","lab.previewConfidence":"置信度","lab.grid":"网格","lab.zoomIn":"放大","lab.zoomOut":"缩小","lab.zoomReset":"复位","lab.resolution":"分辨率","lab.fwhm":"FWHM","lab.starsDetected":"检测星点","lab.meta.title":"拍摄与文件信息","lab.meta.noSidecar":"无侧车信息(非调试采集或仅本地上传)","lab.meta.partial":"暂无侧车详细字段,仅显示文件信息。","lab.solveSection":"解算","lab.imageSection":"图像","lab.metric.solveMs":"用时","lab.metric.solveComputeMs":"解算计算用时","lab.metric.solveComputeHelp":"服务端 Tetra3 与提星等纯计算耗时(与网络无关)。","lab.metric.solveRoundTripMs":"全链路用时","lab.metric.solveRoundTripHelp":"从本页发起请求到收到结果并完成界面刷新的总耗时,含网络往返与浏览器渲染。","lab.metric.backendTotalMs":"后端总用时","lab.metric.openDecodeMs":"读取/解码","lab.metric.preprocessMs":"预处理","lab.metric.extractMs":"提星","lab.metric.solveOnlyMs":"匹配解算","lab.metric.probHelp":"Tetra3 的 Prob 为假阳性概率(越低越可信)。一般使用 (1−Prob)×100%;当 Prob 极小时浮点数会舍入为 100%,此时用 −log₁₀(Prob) 映射到约 70–100% 以便区分。","lab.metric.probRawHelp":"Tetra3 返回的原始 Prob 字段,可能为对数似然等内部量;与上一行换算后的百分比对照查看即可。","lab.metric.radec":"RA / Dec","lab.metric.matches":"匹配","lab.metric.rmse":"RMSE","lab.metric.prob":"置信","lab.metric.status":"状态","lab.gateNextMs":"约 {ms} ms 后可重试","lab.gate.skipRecording":"录制中:相机正写入录像,暂无法从相机取帧解算;请先停止录制后再试。","lab.gate.skipBusyInFlight":"上一帧解算仍在处理中,请稍候。若频繁出现,可适当增大「实时解算间隔」。","lab.gate.skipInterval":"解算请求过于频繁,已按后端节流排队。","lab.gate.skipBusyFallback":"实时解算暂时无法开始({detail})。","lab.centroidQualityTitle":"质心质量","lab.centroidQualityMetrics":"输入 {in} → 保留 {out};剔除过密 {dense},共线 {line}","meta.exposure":"曝光","meta.gain":"增益","meta.fps":"帧率","meta.sensor":"传感器","meta.colorMode":"色彩","meta.outputResolution":"输出分辨率","meta.fileTime":"文件时间","meta.fileSize":"文件大小","results.viewRaw":"原始 JSON","results.hideRaw":"收起","results.solveHistoryTitle":"解算历史","results.historySingle":"单帧","results.historyBatch":"批量 {count} 组","params.title":"解算参数","params.blockSolveIntro":"以下为板块求解(Tetra3)搜索天区、超时与粗略指向提示;FOV 需与镜头视场大致一致。","params.centroid":"提星","params.blockCentroidIntro":"以下为星点检测:阈值、连通域面积与局部背景窗口,用于从图像中提取星点质心。","params.fov":"FOV 估计 (°)","params.fovHelp":"水平视场角估计值,用于 lost-in-space 解算。","params.fovErr":"FOV 允许误差 (°)","params.fovErrHelp":"允许 Tetra3 在估计 FOV 附近的搜索范围。","params.timeout":"超时 (ms)","params.timeoutHelp":"单次解算最长等待时间。","params.solveIntervalMs":"实时解算间隔 (ms)","params.solveIntervalMsHelp":"期望间隔可调整,但会被后端限制在安全范围内。","params.solveIntervalIndependent":"解算间隔独立于相机采集和预览帧率。","params.solveIntervalBound":"后端限制范围:{min}-{max} ms,当前生效:{effective} ms","params.centroidRejectionLevel":"质心剔除强度 (1–5)","params.centroidRejectionLevelHelp":"1 最保守保留更多星点,5 更激进剔除过密与共线假星;默认 3。","params.centroidRejectionScale":"保守 ← → 激进","params.solveProfile":"解算档位","params.solveProfileHelp":"速度/平衡/稳健三档会同时调整超时、提星阈值与参与匹配星点数。","params.solveProfileSpeed":"速度优先","params.solveProfileBalanced":"平衡","params.solveProfileRobust":"稳健优先","params.ra":"RA 提示 (°)","params.raHelp":"大致天球赤经,缩小搜索范围(度)。","params.dec":"Dec 提示 (°)","params.decHelp":"大致天球赤纬(度)。","params.maxSide":"提星前长边上界 (px)","params.maxSideHelp":"降采样长边上限,大图可加速提星。","params.detailLevelFull":"包含完整 Tetra3 原始结果(体积略大,仅调试时开启)","params.largeScaleBg":"大尺度背景减除","params.largeScaleBgHelp":"在提星前用低分辨率平滑估计并校正大尺度亮度不均,可减轻角部光晕导致的假星;默认关闭以保持与过往行为一致。","params.sigma":"σ(阈值倍数)","params.sigmaHelp":"高于背景噪声倍数的区域视为星点候选。","params.maxArea":"max_area","params.maxAreaHelp":"连通域最大像素面积。","params.minArea":"min_area","params.minAreaHelp":"连通域最小像素面积。","params.filtsize":"filtsize(奇数)","params.filtsizeHelp":"局部背景滤波窗口边长,须为奇数。","btn.solveOne":"单张解算","btn.solveBatch":"批量解算(勾选预设)","btn.applyPresets":"应用预设到表单","btn.savePreset":"保存","placeholder.newPreset":"新预设名称","pool.title":"服务器素材池","pool.col.name":"文件名","pool.col.source":"来源","pool.col.size":"大小","pool.col.time":"修改时间","pool.delete":"删除","history.title":"实验记录","history.intro":"此处展示你在解算台完成解算后手动保存的快照。用法:在「解算台」主栏「结果对比」中,单张解算后点「保存当前到实验记录」,或批量解算后在某张结果卡片上点「保存记录」。本页可按文件名或预设名搜索,支持导出 JSON/CSV 备份。","history.search":"搜索…","history.searchBtn":"搜索","history.exportJson":"导出 JSON","history.exportCsv":"导出 CSV","history.total":"共 {n} 条","history.preset":"预设","history.metrics":"指标","history.detail":"详情","history.collapse":"收起","history.prev":"上一页","history.next":"下一页","history.delete":"删除","delete.uploadFirst":"确定要从素材池删除「{name}」吗?","delete.uploadSecond":"此操作不可恢复,再次确认删除?","delete.experimentFirst":"确定要删除这条实验记录吗?","delete.experimentSecond":"此操作不可恢复,再次确认删除?","results.title":"结果对比","results.saveCurrent":"保存当前到实验记录","results.saveRow":"保存记录","results.expand":"展开","results.collapseJson":"收起","err.selectFile":"请选择素材","err.selectPresets":"请勾选至少一个预设","common.placeholder":"—","delete.uploadCascade":"该素材有 {n} 条实验记录,是否一并删除?","lab.solveCurrentFrame":"解算当前帧(文件)","lab.solveFileStart":"开始文件连续解算","lab.solveFileStop":"停止文件连续解算","lab.cameraPreviewLoading":"正在连接共享预览…","lab.solveCameraFrame":"解算相机当前帧","lab.solveCameraStart":"开始相机解算","lab.solveCameraStop":"停止相机解算","lab.videoPreviewFailed":"该视频格式可能不受浏览器支持,建议使用 MP4(H.264) 或 WebM。","lab.previewModeFile":"素材文件","lab.previewModeCamera":"设备相机","lab.stopCameraPreview":"停止预览","lab.videoLiveIntro":"与调试控制台共用同一相机;此处可预览并解算实时帧,无需单独打开调试页。两页可同时使用。","lab.videoMjpegHint":"MJPEG 同时连接数已满:请关闭其它页面的实时预览或稍后重试(默认允许约 4 路,可在 systemd 调整 OGSCOPE_STREAM_MAX_MJPEG_CLIENTS)。","lab.cameraStreamStatusFail":"无法读取视频流名额状态,请稍后重试。","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"原始 Prob","lab.systemLoad":"系统负载","results.saveBatchAll":"保存全部到实验记录","lab.transcode.title":"AVI 文件需先转码","lab.transcode.desc":"当前视频为 AVI。为保证浏览器可预览与连续解算,请先在本地转码为 MP4 并上传替换。上传成功后将自动删除服务器上的原 AVI。","lab.transcode.button":"转码并上传替换","lab.transcode.loading":"正在加载转码器…","lab.transcode.writingBuffer":"正在写入转码缓冲区…","lab.transcode.running":"正在转码 AVI -> MP4…","lab.transcode.packaging":"正在封装转码结果…","lab.transcode.uploading":"正在上传转码结果…","lab.transcode.done":"转码并替换完成,可继续预览与解算。","lab.transcode.failed":"转码或上传失败,请重试。","lab.transcode.fetchFailed":"无法读取服务器上的 AVI 文件。","sys.shell.subtitle":"系统调试控制台","sys.shell.nav.overview":"总览","sys.shell.nav.network":"网络","sys.shell.nav.camera":"相机调试","sys.shell.nav.analysis":"寻星控制台","sys.shell.nav.sensors":"传感器","sys.shell.nav.power":"电源","sys.shell.nav.hmi":"人机交互","sys.shell.nav.config":"配置管理","sys.shell.workbench":"WORKBENCH / 系统工作台","sys.shell.node":"节点","sys.shell.top.overview":"系统状态","sys.shell.top.network":"网络与 WiFi","sys.shell.top.sensors":"传感器","sys.shell.top.power":"电源管理","sys.shell.top.hmi":"人机交互","sys.shell.top.config":"配置管理","sys.overview.breadcrumb.console":"控制台","sys.overview.breadcrumb.module":"系统总览","sys.overview.title":"系统总览","sys.overview.subtitle":"实时系统运行健康状况","sys.overview.metric.cpu":"CPU 使用率","sys.overview.metric.mem":"内存占用","sys.overview.metric.temp":"核心温度","sys.overview.metric.wifi":"WiFi 链路","sys.overview.metric.uptime":"运行时长","sys.overview.metric.load":"1 分钟负载","sys.overview.metric.storage":"存储","sys.overview.linkActive":"链路在线","sys.overview.tempState":"状态: 正常","sys.overview.wifiSummary":"无线网络摘要","sys.overview.iface":"接口","sys.overview.signal":"信号","sys.overview.quality":"质量","sys.overview.storageComingSoon":"即将接入","sys.logs.title":"系统日志","sys.logs.kernel":"内核","sys.logs.liveToggle":"实时拉取","sys.logs.refresh":"刷新","sys.logs.liveOffHint":"实时拉取关闭(默认)","sys.logs.loading":"日志加载中...","sys.logs.empty":"暂无日志","sys.placeholder.breadcrumb":"控制台 / 预留模块","sys.placeholder.block":"预留模块","sys.placeholder.desc":"模块壳层已预留,后续将按统一组件体系接入真实数据。","sys.placeholder.status":"STATUS: RESERVED / 当前为结构化占位页面。","sys.placeholder.sensors.title":"传感器诊断台","sys.placeholder.sensors.desc":"用于 IMU、温湿度、电流电压、姿态与时序采样的统一监控面板。","sys.placeholder.sensors.block1":"实时曲线区","sys.placeholder.sensors.block2":"设备健康卡","sys.placeholder.sensors.block3":"阈值告警区","sys.sensors.title":"传感器诊断台","sys.sensors.desc":"磁力计罗盘指北与 MPU-6050 陀螺仪角速度可视化;底层仍为开发者调试 API。","sys.sensors.mag.section":"磁力计(AK09911 / I²C)","sys.sensors.mag.note":"默认总线 1、地址 12(0x0C,CAD 接 GND)。无需在应用层“定义 GPIO”;请确保固件已启用 I²C 且已加载 i2c-dev。","sys.sensors.mag.bus":"I²C 总线号","sys.sensors.mag.addr":"7-bit 地址(十进制,12=0x0C)","sys.sensors.mag.i2cdetect":"附带运行 i2cdetect","sys.sensors.mag.btnSelftest":"运行自检","sys.sensors.mag.btnProbe":"扫描各总线","sys.sensors.mag.btnCalStart":"开始方向校准","sys.sensors.mag.btnCalCommit":"保存并锁定方向","sys.sensors.mag.btnCalReset":"重置到自动模式","sys.sensors.mag.btnCalStatus":"查看校准状态","sys.sensors.mag.running":"执行中…","sys.sensors.mpu.section":"IMU(MPU-6050 / I²C)","sys.sensors.mpu.note":"默认总线 1、地址 104(0x68,AD0 接 GND)。含陀螺仪角速度采样(MPU 内集成加速度计与陀螺仪)。","sys.sensors.mpu.addr":"7-bit 地址(十进制,104=0x68)","sys.sensors.mpu.btnSelftest":"运行 MPU 自检","sys.sensors.mpu.running":"执行中…","sys.sensors.gyro.title":"陀螺仪(MPU-6050)","sys.sensors.gyro.subtitle":"使用上方同一总线/地址;一次读取加速度与陀螺仪,估算横滚/俯仰并显示 3D 板卡示意。","sys.sensors.gyro.btn":"读取角速度","sys.sensors.gyro.loading":"采样中…","sys.sensors.gyro.dps":"角速度","sys.sensors.gyro.raw":"原始值","sys.sensors.gyro.unitDps":"°/s,±250°/s 量程、默认灵敏度","sys.sensors.gyro.unitRaw":"16-bit 有符号","sys.sensors.gyro.hint":"点击「读取角速度」从 IMU 采样(加速度 0x3B + 陀螺仪 0x43)。横滚/俯仰来自重力;ωz 为绕竖直轴角速度,并非绝对航向。","sys.sensors.gyro.errUnknown":"采样失败","sys.sensors.gyro.live":"连续刷新(约 2 Hz)","sys.sensors.gyro.barsTitle":"角速度条(相对 ±250°/s 满偏)","sys.sensors.gyro.rawBlock":"寄存器原始值(16-bit)","sys.sensors.gyro.att3dTitle":"3D 姿态示意","sys.sensors.gyro.att3dDesc":"横滚/俯仰由加速度计重力估算;ωz 为陀螺仪绕竖直轴角速度(非磁航向)。机体系三轴与板卡固连,随姿态在三维空间中转动。","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"左侧为与电路板固连的机体系:琥珀 +X、天蓝 +Y、紫 +Z(法向);短划为负向参考,整体随横滚/俯仰旋转。","sys.sensors.gyro.att3dRefTitle":"正交轴参考(固定视角)","sys.sensors.gyro.att3dRefDesc":"与左侧同色对应,便于辨认三轴空间关系。","sys.sensors.gyro.roll":"横滚","sys.sensors.gyro.pitch":"俯仰","sys.sensors.gyro.yawRate":"偏航角速度","sys.sensors.compass.tapeCaption":"水平航向带 · 中心为当前读数","sys.sensors.compass.title":"磁力计 · 指北罗盘","sys.sensors.compass.desc":"根据水平面磁场分量估算方向角,用于调试「是否大致指向北方」;精对准需校准与固定安装姿态。","sys.sensors.compass.btn":"刷新罗盘","sys.sensors.compass.loading":"测量中…","sys.sensors.compass.live":"连续刷新","sys.sensors.compass.headingLabel":"水平航向角","sys.sensors.compass.headingHint":"由 atan2(Hx, Hy) 得到,单位度","sys.sensors.compass.cardN":"北","sys.sensors.compass.cardE":"东","sys.sensors.compass.cardS":"南","sys.sensors.compass.cardW":"西","sys.sensors.compass.err":"采样失败","sys.sensors.compass.footnote":"红色针尖表示 XY 平面内磁场水平分量方向;转动设备时指针应随之变化。精对准需软铁/硬铁校准。","sys.sensors.jsonToggle":"展开原始 JSON(排障用)","sys.hmi.title":"人机交互 · SPI 显示","sys.hmi.desc":"通过硬件平面驱动 ST7796(默认 320×320);需 OGSCOPE_DISPLAY_ENABLED=true 且系统已启用 SPI(/dev/spidev0.0)。","sys.hmi.status.section":"显示与硬件平面状态","sys.hmi.status.refresh":"刷新状态","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"存在","sys.hmi.status.no":"不存在","sys.hmi.status.resolution":"分辨率 / DC","sys.hmi.status.driver":"驱动句柄","sys.hmi.status.open":"已打开","sys.hmi.status.closed":"未打开","sys.hmi.status.screenOutput":"逻辑输出","sys.hmi.status.on":"开","sys.hmi.status.off":"关","sys.hmi.status.lastPattern":"上次图案","sys.hmi.status.lastError":"上次错误","sys.hmi.actions.section":"输出到屏幕","sys.hmi.actions.hint":"以下为硬件平面 device.command → hmi;大屏刷新使用较长 RPC 超时。","sys.hmi.actions.smoke":"测试画面(文字+边框)","sys.hmi.actions.colorbars":"彩条","sys.hmi.actions.fill":"填充 RGB","sys.hmi.actions.screenOn":"允许屏幕输出","sys.hmi.actions.screenOff":"暂停屏幕输出","sys.hmi.actions.release":"释放驱动(关闭 SPI/GPIO)","sys.hmi.rawJson":"原始 JSON 响应","sys.placeholder.hmi.title":"人机交互台","sys.placeholder.hmi.desc":"用于 SPI 显示、按键矩阵、背光/对比度与界面回放的调试工作区。","sys.placeholder.hmi.block1":"显示预览区","sys.placeholder.hmi.block2":"输入事件流","sys.placeholder.hmi.block3":"参数与模式控制","sys.placeholder.power.title":"电源与功耗台","sys.placeholder.power.desc":"用于电池、充电、瞬时功耗与热管理策略联动调试。","sys.placeholder.power.block1":"功耗看板","sys.placeholder.power.block2":"电源策略表","sys.placeholder.power.block3":"恢复与保护动作","cam.title":"相机调试控制台","cam.subtitle":"迁移到统一后台架构,保留关键性能优化","cam.btn.system":"系统后台","cam.btn.analysis":"寻星控制台","cam.btn.home":"首页","cam.btn.start":"启动预览","cam.btn.starting":"启动中...","cam.btn.stop":"停止预览","cam.btn.stopping":"停止中...","cam.btn.capture":"拍摄","cam.btn.capturing":"拍摄中...","cam.btn.recordStart":"开始录制","cam.btn.recordStop":"停止录制","cam.btn.recordBusy":"录制切换中...","cam.btn.refresh":"刷新状态","cam.preview.title":"实时预览","cam.preview.state":"状态","cam.preview.mode":"曝光模式","cam.preview.emptyTitle":"预览尚未启动","cam.preview.emptyDesc":"点击下方按钮启动相机预览流。","cam.state.streaming":"流已启动","cam.state.idle":"未启动","cam.state.rec":"录制中","cam.stats.requestFps":"请求 FPS","cam.hint.mjpegSingleStream":"MJPEG 同时连接数有限(默认约两路);超出上限时多标签页会争用或返回忙,请减少同时打开的实时预览。","cam.stats.frameFps":"画面 FPS","cam.stats.fpsMeasureNote":"统计最近约 1 秒内在 32×32 采样上的像素变化次数;多路同时预览、网络较慢、或下方「共享流上限」低于相机目标时,读数常明显偏低。","cam.err.streamBusy":"无法连接预览:MJPEG 名额已被占用","cam.err.streamBusyHint":"请关闭其它标签页或解算台中的设备实时预览,或稍后再试。","cam.err.streamProbeFailed":"无法建立预览流","cam.err.streamProbeDetail":"请检查网络与相机服务状态,或稍后重试。","cam.stats.targetFps":"相机采集目标 FPS","cam.stats.streamPacingFps":"共享流/MJPEG 上限 FPS","cam.stats.streamPacingHint":"与进程环境 OGSCOPE_SHARED_PREVIEW_FPS 一致,约束共享抓帧节奏与每路 MJPEG 的最小帧间隔。","cam.stats.captureFps":"实际采集 FPS","cam.stats.encodeMs":"JPEG 平均编码","cam.stats.encoder":"预览编码器","cam.stats.driver":"相机驱动","cam.stats.metadata":"元数据","cam.stats.consumers":"预览/分析/录像消费者","cam.stats.cameraMemory":"进程内存/Swap","cam.stats.longExposureThrottle":"自动曝光约 {exposure} ms,实际帧率受长曝光限制;为保证分析效果未强制缩短曝光。","cam.stats.uptime":"流运行时长","cam.controls.title":"参数控制","cam.controls.core":"核心参数","cam.controls.fps":"帧率","cam.controls.sensorFps":"传感器目标 FPS","cam.controls.previewFps":"预览目标 FPS","cam.controls.resolution":"分辨率","cam.controls.applyRes":"应用分辨率","cam.controls.sampling":"采样模式","cam.controls.exposure":"曝光","cam.controls.gain":"模拟增益","cam.controls.digitalGain":"数字增益","cam.controls.noiseReduction":"降噪级别","cam.controls.noiseReductionMode":"降噪模式","cam.controls.digitalGainReadOnly":"当前驱动仅上报数字增益,不支持手动设置","cam.controls.aeFlicker":"AE 防闪烁","cam.controls.maxAeFrame":"最长自动曝光帧周期(us)","cam.controls.contrast":"对比度","cam.controls.brightness":"亮度","cam.controls.saturation":"饱和度","cam.controls.sharpness":"锐度","cam.controls.applySettings":"应用全部设置","cam.controls.applyAll":"保存参数","cam.controls.applyRuntime":"保存运行参数","cam.controls.mode":"模式控制","cam.controls.autoExposure":"曝光模式","cam.controls.whiteBalance":"白平衡","cam.controls.colorMode":"颜色模式","cam.controls.auto":"自动","cam.controls.manual":"手动","cam.controls.night":"夜间","cam.controls.off":"关闭","cam.controls.nr.off":"关闭","cam.controls.nr.fast":"快速","cam.controls.nr.high_quality":"高质量","cam.controls.wb.auto":"自动","cam.controls.wb.daylight":"日光","cam.controls.wb.cloudy":"阴天","cam.controls.wb.tungsten":"钨丝灯","cam.controls.wb.fluorescent":"荧光灯","cam.controls.wb.indoor":"室内","cam.controls.wb.manual":"手动","cam.controls.wb.night":"夜间","cam.controls.color":"彩色","cam.controls.mono":"黑白","cam.controls.applyAe":"应用曝光模式","cam.controls.applyWb":"应用白平衡","cam.controls.applyColor":"应用颜色模式","cam.controls.applyMode":"应用模式设置","cam.controls.lockedByAe":"自动曝光开启时,曝光与增益参数锁定。","cam.controls.lockedByWb":"当前非手动白平衡模式,R/B 增益参数锁定。","cam.controls.pendingChanges":"有未应用的参数修改","cam.controls.tools":"工具","cam.controls.nightPreset":"夜间预设","cam.controls.nightOn":"开启夜间模式","cam.controls.nightOff":"关闭夜间模式","cam.controls.backup":"备份设置","cam.controls.restore":"恢复设置","cam.controls.reset":"重置相机","cam.quick.title":"快速预设与智能调参","cam.quick.daylight":"白天模式","cam.quick.night":"夜间模式","cam.quick.nightPreset":"应用夜间预设","cam.quick.deep-sky":"深空模式","cam.quick.planetary":"行星模式","cam.quick.autoAdjust":"智能调整","cam.quick.nightHint":"夜间预设用于一键切换夜拍参数;夜间模式开关用于实时启停红外/夜景模式。","cam.system.title":"系统监控","cam.system.status":"相机就绪","cam.system.stream":"流状态","cam.system.sensor":"传感器","cam.system.quality":"图像质量","cam.presets.title":"预设管理","cam.presets.name":"预设名称","cam.presets.desc":"预设描述","cam.presets.save":"保存预设","cam.presets.empty":"暂无预设","cam.presets.noDesc":"无描述","cam.presets.apply":"应用","cam.presets.delete":"删除","cam.files.title":"文件管理","cam.files.refresh":"刷新文件列表","cam.files.empty":"暂无文件","cam.files.download":"下载","cam.files.info":"详情","cam.files.delete":"删除","cam.files.loadingInfo":"正在读取文件详情...","cam.files.prev":"上一页","cam.files.next":"下一页","cam.files.page":"第 {current}/{total} 页","cam.files.closeDetail":"关闭详情","cam.files.size":"文件大小","cam.files.type":"文件类型","cam.files.modified":"修改时间","cam.hist.title":"直方图","cam.hist.enabled":"启用直方图","cam.hist.luminance":"亮度通道","cam.hist.over":"过曝警告","cam.hist.expand":"展开直方图","cam.hist.collapse":"收起直方图","cam.notice.previewStart":"预览已启动","cam.notice.previewStop":"预览已停止","cam.notice.captureSaved":"拍摄完成: {name}","cam.notice.recordStart":"开始录制: {name}","cam.notice.recordStop":"录制已停止","cam.notice.fpsApplied":"帧率已设置为 {fps}","cam.notice.resApplied":"分辨率已设置为 {w}x{h}","cam.notice.samplingApplied":"采样模式已设置为 {mode}","cam.notice.settingsApplied":"参数已应用","cam.notice.aeApplied":"曝光模式已应用","cam.notice.wbApplied":"白平衡已应用","cam.notice.colorApplied":"颜色模式已应用","cam.notice.modeApplied":"模式设置已应用","cam.notice.runtimeApplied":"运行参数已应用","cam.notice.rotationApplied":"旋转已设置为 {value}°","cam.mirror.hint":"相机倒装时可配合旋转使用;镜像后预览与星点解算、极轴引导同坐标系。","cam.mirror.horizontal":"水平镜像","cam.mirror.vertical":"垂直镜像","cam.notice.mirrorApplied":"镜像设置已更新","cam.notice.nightOn":"夜间模式已开启","cam.notice.nightOff":"夜间模式已关闭","cam.notice.nightPreset":"夜间预设已应用","cam.notice.reset":"相机设置已重置","cam.notice.backup":"设置已备份","cam.notice.restore":"设置已恢复","cam.notice.presetSaved":"预设已保存: {name}","cam.notice.presetApplied":"预设已应用: {name}","cam.notice.presetDeleted":"预设已删除: {name}","cam.notice.download":"已开始下载: {name}","cam.notice.downloadWithSidecar":"已下载: {name}(含侧车 {sidecar})","cam.notice.fileDeleted":"文件已删除: {name}","cam.notice.quickPresetApplied":"已应用快速预设: {preset}","cam.notice.needPreview":"请先启动预览","cam.notice.needManualForAutoAdjust":"请先切换为手动曝光再进行智能调整","cam.notice.autoAdjusted":"智能调整已完成并生效","cam.confirm.reset":"将重置当前设备的相机设置。确认继续?","cam.confirm.deletePreset":"确定删除预设 {name} 吗?","cam.confirm.deleteFile":"确定删除文件 {name} 吗?","sys.sensors.mag.calStatusPrefix":"校准状态:","sys.sensors.mag.calModeAuto":"自动模式(未锁定)","sys.sensors.mag.calModeRecording":"录制中(请旋转设备)","sys.sensors.mag.calModeLocked":"已锁定(使用保存方向)","sys.sensors.mag.calSamplesPrefix":"录制样本:","sys.sensors.mag.calRecordingHint":"操作提示:保持水平,缓慢旋转设备,建议累计 10+ 样本后保存。","sys.sensors.mag.calLockedHint":"已锁定方向参数:"},g=a.createContext(null),p={zh:C,en:k},b="ogscope.analysis.locale";function y(){const e=window.localStorage.getItem(b);return e==="zh"||e==="en"?e:(navigator.language||"zh").toLowerCase().startsWith("en")?"en":"zh"}function R({children:e}){const[s,t]=a.useState(y),[o,l]=a.useState(p[y()]);a.useEffect(()=>{l(p[s]),window.localStorage.setItem(b,s),document.documentElement.lang=s==="en"?"en":"zh-CN"},[s]);const r=a.useMemo(()=>(n,c)=>{let i=o[n]??n;if(c)for(const[d,h]of Object.entries(c))i=i.replace(new RegExp(`\\{${d}\\}`,"g"),String(h));return i},[o]),m=a.useMemo(()=>({locale:s,setLocale:t,t:r}),[s,r]);return v.jsx(g.Provider,{value:m,children:e})}function x(){const e=a.useContext(g);if(!e)throw new Error("useI18n must be used within I18nProvider");return e}export{R as I,A as T,P as c,x as u}; diff --git a/web/static/analysis-lab/assets/index-Cm13xeJE.css b/web/static/analysis-lab/assets/index-Cm13xeJE.css new file mode 100644 index 0000000..45e5428 --- /dev/null +++ b/web/static/analysis-lab/assets/index-Cm13xeJE.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-3{left:.75rem}.left-\[10px\]{left:10px}.left-\[110px\]{left:110px}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-12{top:3rem}.top-2{top:.5rem}.top-3{top:.75rem}.top-8{top:2rem}.top-\[18px\]{top:18px}.top-\[88px\]{top:88px}.top-\[94px\]{top:94px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.order-1{order:1}.order-3{order:3}.col-span-12{grid-column:span 12 / span 12}.col-span-2{grid-column:span 2 / span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-0{height:0px}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[100px\]{height:100px}.h-\[112px\]{height:112px}.h-\[152px\]{height:152px}.h-\[188px\]{height:188px}.h-\[460px\]{height:460px}.h-\[88px\]{height:88px}.h-\[calc\(100vh-7rem\)\]{height:calc(100vh - 7rem)}.h-dvh{height:100dvh}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-32{max-height:8rem}.max-h-36{max-height:9rem}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-96{max-height:24rem}.max-h-\[320px\]{max-height:320px}.max-h-\[420px\]{max-height:420px}.max-h-\[70vh\]{max-height:70vh}.max-h-\[min\(22rem\,55vh\)\]{max-height:min(22rem,55vh)}.max-h-\[min\(36vh\,18rem\)\]{max-height:min(36vh,18rem)}.max-h-\[min\(50vh\,28rem\)\]{max-height:min(50vh,28rem)}.max-h-dvh{max-height:100dvh}.min-h-0{min-height:0px}.min-h-14{min-height:3.5rem}.min-h-\[120px\]{min-height:120px}.min-h-\[200px\]{min-height:200px}.min-h-\[220px\]{min-height:220px}.min-h-\[480px\]{min-height:480px}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-0\.5{width:.125rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[100px\]{width:100px}.w-\[200px\]{width:200px}.w-\[220px\]{width:220px}.w-\[360px\]{width:360px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[12rem\]{min-width:12rem}.min-w-\[15rem\]{min-width:15rem}.min-w-\[16rem\]{min-width:16rem}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[1880px\]{max-width:1880px}.max-w-\[220px\]{max-width:220px}.max-w-\[calc\(100\%-1\.5rem\)\]{max-width:calc(100% - 1.5rem)}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.border-separate{border-collapse:separate}.border-spacing-y-2{--tw-border-spacing-y: .5rem;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-top-left{transform-origin:top left}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-px{--tw-translate-x: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-crosshair{cursor:crosshair}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.scroll-mt-20{scroll-margin-top:5rem}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.gap-y-2{row-gap:.5rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.self-start{align-self:flex-start}.self-center{align-self:center}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-x-\[9px\]{border-left-width:9px;border-right-width:9px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-b-\[12px\]{border-bottom-width:12px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-r-2{border-right-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/40{border-color:#fbbf2466}.border-amber-500\/40{border-color:#f59e0b66}.border-emerald-400\/40{border-color:#34d39966}.border-emerald-500\/20{border-color:#10b98133}.border-emerald-600\/35{border-color:#05966959}.border-error\/35{border-color:#ffb4ab59}.border-error\/40{border-color:#ffb4ab66}.border-error\/50{border-color:#ffb4ab80}.border-error\/60{border-color:#ffb4ab99}.border-outline-variant\/10{border-color:#4147541a}.border-outline-variant\/15{border-color:#41475426}.border-outline-variant\/20{border-color:#41475433}.border-outline-variant\/25{border-color:#41475440}.border-outline-variant\/30{border-color:#4147544d}.border-outline-variant\/40{border-color:#41475466}.border-primary{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.border-primary\/30{border-color:#adc6ff4d}.border-primary\/40{border-color:#adc6ff66}.border-primary\/45{border-color:#adc6ff73}.border-primary\/50{border-color:#adc6ff80}.border-primary\/70{border-color:#adc6ffb3}.border-red-500\/40{border-color:#ef444466}.border-rose-400\/40{border-color:#fb718566}.border-sky-500\/30{border-color:#0ea5e94d}.border-teal-400\/75{border-color:#2dd4bfbf}.border-teal-500\/25{border-color:#14b8a640}.border-white\/20{border-color:#fff3}.border-white\/30{border-color:#ffffff4d}.border-white\/5{border-color:#ffffff0d}.border-x-transparent{border-left-color:transparent;border-right-color:transparent}.border-b-amber-400{--tw-border-opacity: 1;border-bottom-color:rgb(251 191 36 / var(--tw-border-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-400\/95{background-color:#fbbf24f2}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-900\/55{background-color:#78350f8c}.bg-background{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/20{background-color:#0003}.bg-black\/25{background-color:#00000040}.bg-black\/35{background-color:#00000059}.bg-black\/40{background-color:#0006}.bg-black\/55{background-color:#0000008c}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-black\/80{background-color:#000c}.bg-emerald-800\/90{background-color:#065f46e6}.bg-error-container\/15{background-color:#93000a26}.bg-error-container\/20{background-color:#93000a33}.bg-error-container\/95{background-color:#93000af2}.bg-error\/10{background-color:#ffb4ab1a}.bg-error\/80{background-color:#ffb4abcc}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-neutral-950\/80{background-color:#0a0a0acc}.bg-primary{--tw-bg-opacity: 1;background-color:rgb(173 198 255 / var(--tw-bg-opacity, 1))}.bg-primary-container{--tw-bg-opacity: 1;background-color:rgb(76 142 255 / var(--tw-bg-opacity, 1))}.bg-primary-container\/10{background-color:#4c8eff1a}.bg-primary-container\/20{background-color:#4c8eff33}.bg-primary-container\/30{background-color:#4c8eff4d}.bg-primary-container\/50{background-color:#4c8eff80}.bg-primary\/10{background-color:#adc6ff1a}.bg-primary\/20{background-color:#adc6ff33}.bg-primary\/5{background-color:#adc6ff0d}.bg-red-500\/10{background-color:#ef44441a}.bg-secondary{--tw-bg-opacity: 1;background-color:rgb(64 229 108 / var(--tw-bg-opacity, 1))}.bg-secondary\/80{background-color:#40e56ccc}.bg-sky-300\/90{background-color:#7dd3fce6}.bg-sky-400{--tw-bg-opacity: 1;background-color:rgb(56 189 248 / var(--tw-bg-opacity, 1))}.bg-sky-400\/95{background-color:#38bdf8f2}.bg-sky-700\/80{background-color:#0369a1cc}.bg-sky-900\/50{background-color:#0c4a6e80}.bg-slate-500\/85{background-color:#64748bd9}.bg-slate-500\/90{background-color:#64748be6}.bg-slate-600\/40{background-color:#47556966}.bg-slate-600\/50{background-color:#47556980}.bg-slate-600\/60{background-color:#47556999}.bg-slate-900\/80{background-color:#0f172acc}.bg-slate-950\/50{background-color:#02061780}.bg-surface{--tw-bg-opacity: 1;background-color:rgb(16 19 26 / var(--tw-bg-opacity, 1))}.bg-surface-container{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.bg-surface-container-high{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.bg-surface-container-high\/40{background-color:#272a3166}.bg-surface-container-high\/95{background-color:#272a31f2}.bg-surface-container-highest{--tw-bg-opacity: 1;background-color:rgb(50 53 60 / var(--tw-bg-opacity, 1))}.bg-surface-container-highest\/30{background-color:#32353c4d}.bg-surface-container-highest\/40{background-color:#32353c66}.bg-surface-container-highest\/95{background-color:#32353cf2}.bg-surface-container-low{--tw-bg-opacity: 1;background-color:rgb(25 28 34 / var(--tw-bg-opacity, 1))}.bg-surface-container-low\/70{background-color:#191c22b3}.bg-surface-container-low\/80{background-color:#191c22cc}.bg-surface-container-low\/90{background-color:#191c22e6}.bg-surface-container-low\/95{background-color:#191c22f2}.bg-surface-container-lowest{--tw-bg-opacity: 1;background-color:rgb(11 14 20 / var(--tw-bg-opacity, 1))}.bg-surface-container-lowest\/90{background-color:#0b0e14e6}.bg-surface-container-lowest\/95{background-color:#0b0e14f2}.bg-surface-container\/50{background-color:#1d202680}.bg-surface-container\/60{background-color:#1d202699}.bg-surface-container\/80{background-color:#1d2026cc}.bg-surface-container\/90{background-color:#1d2026e6}.bg-violet-400{--tw-bg-opacity: 1;background-color:rgb(167 139 250 / var(--tw-bg-opacity, 1))}.bg-violet-400\/95{background-color:#a78bfaf2}.bg-violet-900\/45{background-color:#4c1d9573}.bg-white\/5{background-color:#ffffff0d}.bg-white\/80{background-color:#fffc}.bg-white\/90{background-color:#ffffffe6}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-300\/95{--tw-gradient-from: rgb(252 211 77 / .95) var(--tw-gradient-from-position);--tw-gradient-to: rgb(252 211 77 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-emerald-600{--tw-gradient-from: #059669 var(--tw-gradient-from-position);--tw-gradient-to: rgb(5 150 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-600\/90{--tw-gradient-from: rgb(71 85 105 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/40{--tw-gradient-from: rgb(15 23 42 / .4) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-slate-900\/90{--tw-gradient-from: rgb(15 23 42 / .9) var(--tw-gradient-from-position);--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-slate-800\/95{--tw-gradient-to: rgb(30 41 59 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(30 41 59 / .95) var(--tw-gradient-via-position), var(--tw-gradient-to)}.via-slate-900\/70{--tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(15 23 42 / .7) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-sky-400\/40{--tw-gradient-to: rgb(56 189 248 / .4) var(--tw-gradient-to-position)}.to-sky-500{--tw-gradient-to: #0ea5e9 var(--tw-gradient-to-position)}.to-slate-950{--tw-gradient-to: #020617 var(--tw-gradient-to-position)}.to-surface-container\/70{--tw-gradient-to: rgb(29 32 38 / .7) var(--tw-gradient-to-position)}.to-surface-container\/90{--tw-gradient-to: rgb(29 32 38 / .9) var(--tw-gradient-to-position)}.fill-current{fill:currentColor}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-0\.5{padding-bottom:.125rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pr-0\.5{padding-right:.125rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-body,.font-headline{font-family:system-ui,Segoe UI,sans-serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-100{--tw-text-opacity: 1;color:rgb(254 243 199 / var(--tw-text-opacity, 1))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-200\/95{color:#fde68af2}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-emerald-200{--tw-text-opacity: 1;color:rgb(167 243 208 / var(--tw-text-opacity, 1))}.text-emerald-50{--tw-text-opacity: 1;color:rgb(236 253 245 / var(--tw-text-opacity, 1))}.text-error{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.text-on-primary-container{--tw-text-opacity: 1;color:rgb(0 40 93 / var(--tw-text-opacity, 1))}.text-on-surface{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.text-on-surface-variant{--tw-text-opacity: 1;color:rgb(194 198 214 / var(--tw-text-opacity, 1))}.text-on-surface-variant\/70{color:#c2c6d6b3}.text-on-surface-variant\/80{color:#c2c6d6cc}.text-on-surface-variant\/85{color:#c2c6d6d9}.text-on-surface-variant\/90{color:#c2c6d6e6}.text-on-surface-variant\/95{color:#c2c6d6f2}.text-on-surface\/80{color:#e1e2ebcc}.text-primary{--tw-text-opacity: 1;color:rgb(173 198 255 / var(--tw-text-opacity, 1))}.text-primary\/80{color:#adc6ffcc}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-secondary{--tw-text-opacity: 1;color:rgb(64 229 108 / var(--tw-text-opacity, 1))}.text-sky-100{--tw-text-opacity: 1;color:rgb(224 242 254 / var(--tw-text-opacity, 1))}.text-sky-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.text-sky-300\/90{color:#7dd3fce6}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-teal-100{--tw-text-opacity: 1;color:rgb(204 251 241 / var(--tw-text-opacity, 1))}.text-teal-200{--tw-text-opacity: 1;color:rgb(153 246 228 / var(--tw-text-opacity, 1))}.text-teal-200\/95{color:#99f6e4f2}.text-violet-200{--tw-text-opacity: 1;color:rgb(221 214 254 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-primary{accent-color:#adc6ff}.opacity-50{opacity:.5}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_6px_rgba\(255\,255\,255\,0\.6\)\]{--tw-shadow: 0 0 6px rgba(255,255,255,.6);--tw-shadow-colored: 0 0 6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_20px_50px_rgba\(0\,0\,0\,0\.55\)\,inset_0_1px_0_rgba\(255\,255\,255\,0\.08\)\]{--tw-shadow: 0 20px 50px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.08);--tw-shadow-colored: 0 20px 50px var(--tw-shadow-color), inset 0 1px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-amber-200\/35{--tw-ring-color: rgb(253 230 138 / .35)}.ring-sky-200\/30{--tw-ring-color: rgb(186 230 253 / .3)}.ring-sky-500\/25{--tw-ring-color: rgb(14 165 233 / .25)}.ring-slate-600\/50{--tw-ring-color: rgb(71 85 105 / .5)}.ring-teal-500\/10{--tw-ring-color: rgb(20 184 166 / .1)}.ring-violet-200\/35{--tw-ring-color: rgb(221 214 254 / .35)}.ring-white\/5{--tw-ring-color: rgb(255 255 255 / .05)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.will-change-transform{will-change:transform}html,body,#root{height:100%}.og-scrollbar{scrollbar-width:thin;scrollbar-color:#414754 #191c22}.og-scrollbar::-webkit-scrollbar{width:7px;height:7px}.og-scrollbar::-webkit-scrollbar-track{background:#191c22;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb{background:#414754;border-radius:4px}.og-scrollbar::-webkit-scrollbar-thumb:hover{background:#5c6070}.hover\:border-primary:hover{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.hover\:bg-amber-500\/10:hover{background-color:#f59e0b1a}.hover\:bg-emerald-500\/10:hover{background-color:#10b9811a}.hover\:bg-emerald-700:hover{--tw-bg-opacity: 1;background-color:rgb(4 120 87 / var(--tw-bg-opacity, 1))}.hover\:bg-error-container\/20:hover{background-color:#93000a33}.hover\:bg-error-container\/30:hover{background-color:#93000a4d}.hover\:bg-error\/10:hover{background-color:#ffb4ab1a}.hover\:bg-rose-500\/10:hover{background-color:#f43f5e1a}.hover\:bg-sky-600:hover{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container:hover{--tw-bg-opacity: 1;background-color:rgb(29 32 38 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container-high:hover{--tw-bg-opacity: 1;background-color:rgb(39 42 49 / var(--tw-bg-opacity, 1))}.hover\:bg-surface-container\/80:hover{background-color:#1d2026cc}.hover\:bg-white\/5:hover{background-color:#ffffff0d}.hover\:text-error:hover{--tw-text-opacity: 1;color:rgb(255 180 171 / var(--tw-text-opacity, 1))}.hover\:text-on-surface:hover{--tw-text-opacity: 1;color:rgb(225 226 235 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}.focus\:border-primary:focus{--tw-border-opacity: 1;border-color:rgb(173 198 255 / var(--tw-border-opacity, 1))}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:gap-4{gap:1rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width: 768px){.md\:col-span-1{grid-column:span 1 / span 1}.md\:col-span-3{grid-column:span 3 / span 3}.md\:col-span-4{grid-column:span 4 / span 4}.md\:min-w-\[200px\]{min-width:200px}.md\:max-w-md{max-width:28rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:justify-center{justify-content:center}.md\:gap-8{gap:2rem}}@media (min-width: 1024px){.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-4{grid-column:span 4 / span 4}.lg\:col-span-8{grid-column:span 8 / span 8}.lg\:col-span-9{grid-column:span 9 / span 9}.lg\:ml-64{margin-left:16rem}.lg\:hidden{display:none}.lg\:max-w-lg{max-width:32rem}.lg\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:p-6{padding:1.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:max-w-\[340px\]{max-width:340px}.xl\:grid-cols-\[minmax\(0\,1\.25fr\)_minmax\(280px\,0\.75fr\)\]{grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr)}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:gap-8{gap:2rem}.xl\:self-start{align-self:flex-start}}@media (min-width: 1536px){.\32xl\:static{position:static}.\32xl\:order-1{order:1}.\32xl\:order-2{order:2}.\32xl\:col-span-10{grid-column:span 10 / span 10}.\32xl\:col-span-2{grid-column:span 2 / span 2}.\32xl\:col-span-3{grid-column:span 3 / span 3}.\32xl\:col-span-9{grid-column:span 9 / span 9}.\32xl\:hidden{display:none}.\32xl\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\32xl\:self-start{align-self:flex-start}.\32xl\:shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none} diff --git a/web/static/analysis-lab/assets/index-DfDaIsmY.js b/web/static/analysis-lab/assets/index-DfDaIsmY.js new file mode 100644 index 0000000..b9811b8 --- /dev/null +++ b/web/static/analysis-lab/assets/index-DfDaIsmY.js @@ -0,0 +1,26 @@ +import{r as a,j as f}from"./client-D1ZVDB-N.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 v=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),y=(...e)=>e.filter((s,t,o)=>!!s&&s.trim()!==""&&o.indexOf(s)===t).join(" ").trim();/** + * @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. + */var w={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @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 S=a.forwardRef(({color:e="currentColor",size:s=24,strokeWidth:t=2,absoluteStrokeWidth:o,className:i="",children:r,iconNode:m,...n},c)=>a.createElement("svg",{ref:c,...w,width:s,height:s,stroke:e,strokeWidth:o?Number(t)*24/Number(s):t,className:y("lucide",i),...n},[...m.map(([l,d])=>a.createElement(l,d)),...Array.isArray(r)?r:[r]]));/** + * @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 P=(e,s)=>{const t=a.forwardRef(({className:o,...i},r)=>a.createElement(S,{ref:r,iconNode:s,className:y(`lucide-${v(e)}`,o),...i}));return t.displayName=`${e}`,t};/** + * @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 R=P("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]),k={"app.title":"OGScope Plate Solve Console","nav.lab":"Lab","nav.labImage":"Image solve","nav.labVideo":"Video solve","delete.uploadCascade":"Delete {n} linked experiment record(s) as well?","lab.solveCurrentFrame":"Solve current frame (file)","lab.solveFileStart":"Start continuous file solve","lab.solveFileStop":"Stop continuous file solve","lab.cameraPreviewLoading":"Connecting to shared preview…","lab.solveCameraFrame":"Solve live camera frame","lab.solveCameraStart":"Start camera solve","lab.solveCameraStop":"Stop camera solve","lab.videoPreviewFailed":"This video format may be unsupported by the browser. Try MP4 (H.264) or WebM.","lab.previewModeFile":"Pool file","lab.previewModeCamera":"Device camera","lab.videoLiveIntro":"Shares the same camera as Camera Debug — preview and solve live frames here without opening debug. Both pages can run together.","lab.videoMjpegHint":"MJPEG slots are full: close live previews on other pages or retry later (default ~4; tune OGSCOPE_STREAM_MAX_MJPEG_CLIENTS in systemd).","lab.cameraStreamStatusFail":"Could not read MJPEG slot status. Please retry shortly.","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"Raw Prob","lab.systemLoad":"System load","results.saveBatchAll":"Save all to records","nav.pool":"Assets","nav.history":"Records","nav.cameraDebug":"Camera Debug","nav.home":"Home","nav.systemAdmin":"Back to System Admin","lang.zh":"中文","lang.en":"EN","sidebar.assets":"Uploaded assets","sidebar.upload":"Upload","sidebar.refresh":"Refresh","sidebar.debugCaptures":"Debug console media","sidebar.assetTypeImage":"Image","sidebar.assetTypeVideo":"Video","sidebar.debugEmpty":"No debug files","sidebar.importToPool":"Import to pool","sidebar.importToPoolWithTranscode":"Transcode & Import","sidebar.importToPoolDirect":"Import Directly","sidebar.flowPreparing":"Preparing...","sidebar.flowImportingDebug":"Importing debug capture...","sidebar.flowDownloadingPool":"Downloading imported file...","sidebar.flowWritingBuffer":"Writing to transcoder buffer...","sidebar.flowLoadingTranscoder":"Loading transcoder...","sidebar.flowTranscoding":"Transcoding AVI -> MP4...","sidebar.flowPackaging":"Packaging output...","sidebar.flowUploadingMp4":"Uploading MP4...","sidebar.flowReplacing":"Replacing and removing original AVI...","sidebar.flowNoTranscode":"No transcode required, imported directly.","sidebar.flowDone":"Completed","sidebar.flowDoneMsg":"Done: {name}","sidebar.flowFailed":"Failed","sidebar.debugPage":"Page {cur} / {total}","sidebar.batchPresets":"Batch presets","sidebar.batchHint":"Check presets, then use Batch solve to compare multiple param sets.","lab.selectOrUpload":"Pick an uploaded or imported asset from the left","lab.selectOrUploadVideo":"Pick a pool video to preview, or use the button above for the live camera frame.","lab.file":"File","lab.source":"Source","lab.layers":"Layers","lab.layer.matched":"Matched","lab.layer.pattern":"Pattern","lab.layer.all":"All centroids","lab.layer.rejected":"Rejected (red cross)","lab.previewConfidence":"Confidence","lab.grid":"Grid","lab.zoomIn":"Zoom in","lab.zoomOut":"Zoom out","lab.zoomReset":"Reset","lab.resolution":"Resolution","lab.fwhm":"FWHM","lab.starsDetected":"Stars detected","lab.meta.title":"Capture & file info","lab.meta.noSidecar":"No sidecar (not from debug capture)","lab.meta.partial":"No detailed sidecar; file info only.","lab.solveSection":"Solve","lab.imageSection":"Image","lab.metric.solveMs":"Time","lab.metric.solveComputeMs":"Solve compute","lab.metric.solveComputeHelp":"Server-side Tetra3 + star extraction only (no network).","lab.metric.solveRoundTripMs":"End-to-end","lab.metric.solveRoundTripHelp":"From request start to UI updated: network + JSON + render.","lab.metric.backendTotalMs":"Backend total","lab.metric.openDecodeMs":"Open/decode","lab.metric.preprocessMs":"Preprocess","lab.metric.extractMs":"Extract","lab.metric.solveOnlyMs":"Solve match","lab.metric.probHelp":"Tetra3 `Prob` is false-positive probability (lower is better). We use (1−Prob)×100% when representable; for extremely small Prob, IEEE doubles round to 100%, so we map −log10(Prob) to ~70–100% for differentiation.","lab.metric.probRawHelp":"Raw Tetra3 Prob (e.g. log-likelihood); compare with the normalized line above.","lab.metric.radec":"RA / Dec","lab.metric.matches":"Matches","lab.metric.rmse":"RMSE","lab.metric.prob":"Prob.","lab.metric.status":"Status","lab.gateNextMs":"Retry in ~{ms} ms","lab.gate.skipRecording":"Recording is active: the camera is writing video, so live-frame solve is paused. Stop recording and try again.","lab.gate.skipBusyInFlight":"The previous solve is still running; please wait. If this repeats often, increase the realtime solve interval.","lab.gate.skipInterval":"Solve requests are arriving faster than allowed; the server is throttling. Wait for the countdown and retry.","lab.gate.skipBusyFallback":"Realtime solve could not start ({detail}).","lab.centroidQualityTitle":"Centroid quality","lab.centroidQualityMetrics":"In {in} → kept {out}; removed dense {dense}, collinear {line}","meta.exposure":"Exposure","meta.gain":"Gain","meta.fps":"FPS","meta.sensor":"Sensor","meta.colorMode":"Color","meta.outputResolution":"Output size","meta.fileTime":"File time","meta.fileSize":"File size","results.viewRaw":"Raw JSON","results.hideRaw":"Hide","results.solveHistoryTitle":"Solve history","results.historySingle":"Single frame","results.historyBatch":"Batch ({count} runs)","params.title":"Solve parameters","params.blockSolveIntro":"Plate-solve (Tetra3): FOV, timeout, and coarse sky hints. FOV should match your lens.","params.centroid":"Star detection","params.blockCentroidIntro":"Star detection: threshold, blob area, and local background window for centroids.","params.fov":"FOV estimate (°)","params.fovHelp":"Horizontal field of view for lost-in-space solve.","params.fovErr":"FOV max error (°)","params.fovErrHelp":"Search range around estimated FOV.","params.timeout":"Timeout (ms)","params.timeoutHelp":"Max wait time per solve.","params.solveIntervalMs":"Realtime solve interval (ms)","params.solveIntervalMsHelp":"Desired interval is adjustable, but backend clamps it into a safe range.","params.solveIntervalIndependent":"Solve cadence is independent from sensor and preview frame rates.","params.solveIntervalBound":"Backend bounds: {min}-{max} ms, effective now: {effective} ms","params.centroidRejectionLevel":"Centroid rejection (1–5)","params.centroidRejectionLevelHelp":"1 keeps more stars; 5 aggressively rejects dense clusters and collinear false detections; default 3.","params.centroidRejectionScale":"Conservative ← → Aggressive","params.solveProfile":"Solve profile","params.solveProfileHelp":"Speed/Balanced/Robust tune timeout, centroid thresholds, and matching star count together.","params.solveProfileSpeed":"Speed first","params.solveProfileBalanced":"Balanced","params.solveProfileRobust":"Robust first","params.ra":"RA hint (°)","params.raHelp":"Approximate right ascension in degrees.","params.dec":"Dec hint (°)","params.decHelp":"Approximate declination in degrees.","params.maxSide":"Max long side before extract (px)","params.maxSideHelp":"Downscale long edge for faster centroid extraction.","params.detailLevelFull":"Include full Tetra3 raw block (larger payload, for debugging only).","params.largeScaleBg":"Large-scale background flattening","params.largeScaleBgHelp":"Before centroiding, estimate a low-frequency background on a downscaled image and correct uneven illumination (e.g. corner glow). Off by default to match legacy behavior.","params.sigma":"σ threshold","params.sigmaHelp":"Multiplier over background noise for star candidates.","params.maxArea":"max_area","params.maxAreaHelp":"Max connected component area in pixels.","params.minArea":"min_area","params.minAreaHelp":"Min connected component area in pixels.","params.filtsize":"filtsize (odd)","params.filtsizeHelp":"Local filter window size, must be odd.","btn.solveOne":"Solve once","btn.solveBatch":"Batch solve (presets)","btn.applyPresets":"Apply preset to form","btn.savePreset":"Save","placeholder.newPreset":"New preset name","pool.title":"Server asset pool","pool.col.name":"Filename","pool.col.source":"Source","pool.col.size":"Size","pool.col.time":"Modified","pool.delete":"Delete","history.title":"Experiment records","history.intro":"Saved solve snapshots from the Lab. After a solve, use Save to records in the Lab main panel (Result comparison), or Save on each batch result card. Search by filename or preset; export JSON/CSV for backup.","history.search":"Search…","history.searchBtn":"Search","history.exportJson":"Export JSON","history.exportCsv":"Export CSV","history.total":"Total {n}","history.preset":"Preset","history.metrics":"Metrics","history.detail":"Details","history.collapse":"Collapse","history.prev":"Prev","history.next":"Next","history.delete":"Delete","delete.uploadFirst":'Delete "{name}" from the asset pool?',"delete.uploadSecond":"This cannot be undone. Confirm again?","delete.experimentFirst":"Delete this experiment record?","delete.experimentSecond":"This cannot be undone. Confirm again?","results.title":"Results","results.saveCurrent":"Save to records","results.saveRow":"Save","results.expand":"Expand","results.collapseJson":"Collapse","err.selectFile":"Select a file","err.selectPresets":"Select at least one preset","common.placeholder":"—","lab.transcode.title":"AVI requires transcoding","lab.transcode.desc":"This video is AVI. For browser preview and continuous solving, transcode it locally to MP4 and upload replacement. The original AVI on server will be removed after success.","lab.transcode.button":"Transcode and Upload Replacement","lab.transcode.loading":"Loading transcoder...","lab.transcode.writingBuffer":"Writing to transcoder buffer...","lab.transcode.running":"Transcoding AVI -> MP4...","lab.transcode.packaging":"Packaging transcoded output...","lab.transcode.uploading":"Uploading transcoded output...","lab.transcode.done":"Transcode and replacement completed.","lab.transcode.failed":"Transcode or upload failed. Please retry.","lab.transcode.fetchFailed":"Failed to fetch AVI from server.","sys.shell.subtitle":"system debug console","sys.shell.nav.overview":"Overview","sys.shell.nav.network":"Network","sys.shell.nav.camera":"Camera Debug","sys.shell.nav.analysis":"Analysis Console","sys.shell.nav.sensors":"Sensors","sys.shell.nav.power":"Power","sys.shell.nav.hmi":"HMI","sys.shell.nav.config":"Config","sys.shell.workbench":"WORKBENCH / System workspace","sys.shell.node":"Node","sys.shell.top.overview":"System Status","sys.shell.top.network":"Network & WiFi","sys.shell.top.sensors":"Sensors","sys.shell.top.power":"Power Management","sys.shell.top.hmi":"HMI","sys.shell.top.config":"Config","sys.overview.breadcrumb.console":"Console","sys.overview.breadcrumb.module":"System Overview","sys.overview.title":"System Overview","sys.overview.subtitle":"Live system telemetry snapshot","sys.overview.metric.cpu":"CPU Usage","sys.overview.metric.mem":"Memory Usage","sys.overview.metric.temp":"Core Temperature","sys.overview.metric.wifi":"WiFi Link","sys.overview.metric.uptime":"Uptime","sys.overview.metric.load":"Load (1m)","sys.overview.metric.storage":"Storage","sys.overview.linkActive":"Link Active","sys.overview.tempState":"status: nominal","sys.overview.wifiSummary":"WiFi Summary","sys.overview.iface":"Interface","sys.overview.signal":"Signal","sys.overview.quality":"Quality","sys.overview.storageComingSoon":"Coming soon","sys.logs.title":"System Logs","sys.logs.kernel":"Kernel","sys.logs.liveToggle":"Live pull","sys.logs.refresh":"Refresh","sys.logs.liveOffHint":"Live pull is off (default)","sys.logs.loading":"Loading logs...","sys.logs.empty":"No logs","sys.placeholder.breadcrumb":"Console / Reserved Module","sys.placeholder.block":"PLANNED BLOCK","sys.placeholder.desc":"Shell layout is reserved and ready for real data integration.","sys.placeholder.status":"STATUS: RESERVED / Structured placeholder page.","sys.placeholder.sensors.title":"Sensors Console","sys.placeholder.sensors.desc":"Unified monitor for IMU, environment, power rails, and timeline sampling.","sys.placeholder.sensors.block1":"Realtime chart area","sys.placeholder.sensors.block2":"Device health cards","sys.placeholder.sensors.block3":"Threshold alerts","sys.sensors.title":"Sensors Console","sys.sensors.desc":"Visual compass (magnetometer) and gyro rate gauges; backed by dev debug APIs.","sys.sensors.mag.section":"Magnetometer (AK09911 / I²C)","sys.sensors.mag.note":"Defaults: bus 1, address 12 (0x0C with CAD to GND). No app-level GPIO mapping is required; enable I²C in firmware and load i2c-dev.","sys.sensors.mag.bus":"I²C bus","sys.sensors.mag.addr":"7-bit address (decimal, 12 = 0x0C)","sys.sensors.mag.i2cdetect":"Run i2cdetect","sys.sensors.mag.btnSelftest":"Run self-test","sys.sensors.mag.btnProbe":"Probe all buses","sys.sensors.mag.btnCalStart":"Start heading calibration","sys.sensors.mag.btnCalCommit":"Save & lock heading","sys.sensors.mag.btnCalReset":"Reset to auto mode","sys.sensors.mag.btnCalStatus":"Calibration status","sys.sensors.mag.running":"Running…","sys.sensors.mpu.section":"IMU (MPU-6050 / I²C)","sys.sensors.mpu.note":"Defaults: bus 1, address 104 (0x68 with AD0 low). Includes gyro angular-rate sampling (MPU integrates accel + gyro).","sys.sensors.mpu.addr":"7-bit address (decimal, 104 = 0x68)","sys.sensors.mpu.btnSelftest":"Run MPU self-test","sys.sensors.mpu.running":"Running…","sys.sensors.gyro.title":"Gyroscope (MPU-6050)","sys.sensors.gyro.subtitle":"Uses the same bus/address as above; one IMU read: 3-axis gyro, accel-derived roll/pitch, and a 3D board hint.","sys.sensors.gyro.btn":"Read angular rate","sys.sensors.gyro.loading":"Sampling…","sys.sensors.gyro.dps":"Angular rate","sys.sensors.gyro.raw":"Raw","sys.sensors.gyro.unitDps":"°/s, ±250 °/s range, default sensitivity","sys.sensors.gyro.unitRaw":"signed 16-bit","sys.sensors.gyro.hint":"Click “Read angular rate” to sample accel (0x3B) and gyro (0x43). Roll/pitch use gravity; ωz is yaw rate, not absolute heading.","sys.sensors.gyro.errUnknown":"Sample failed","sys.sensors.gyro.live":"Live update (~2 Hz)","sys.sensors.gyro.barsTitle":"Angular rate bars (±250 °/s full scale)","sys.sensors.gyro.rawBlock":"Raw register values (16-bit)","sys.sensors.gyro.att3dTitle":"3D attitude hint","sys.sensors.gyro.att3dDesc":"Roll/pitch from accelerometer (gravity); ωz is yaw rate (not magnetic heading). Body XYZ triad is fixed to the PCB and rotates in 3D.","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"Body frame on the PCB: +X amber, +Y sky, +Z violet (normal); short dashes mark negative directions; the whole frame follows roll/pitch.","sys.sensors.gyro.att3dRefTitle":"Orthogonal axes (fixed view)","sys.sensors.gyro.att3dRefDesc":"Same colors as left; use this to read XYZ directions at a glance.","sys.sensors.gyro.roll":"Roll","sys.sensors.gyro.pitch":"Pitch","sys.sensors.gyro.yawRate":"Yaw rate","sys.sensors.compass.tapeCaption":"Heading tape · center = current readout","sys.sensors.compass.title":"Magnetometer · North compass","sys.sensors.compass.desc":"Horizontal-plane heading for “roughly north” debugging; precise alignment needs calibration and a fixed mount definition.","sys.sensors.compass.btn":"Refresh compass","sys.sensors.compass.loading":"Measuring…","sys.sensors.compass.live":"Live update","sys.sensors.compass.headingLabel":"Horizontal heading","sys.sensors.compass.headingHint":"From atan2(Hx, Hy), degrees","sys.sensors.compass.cardN":"N","sys.sensors.compass.cardE":"E","sys.sensors.compass.cardS":"S","sys.sensors.compass.cardW":"W","sys.sensors.compass.err":"Sample failed","sys.sensors.compass.footnote":"The red tip tracks the horizontal XY field direction; it should move as you rotate the board. Use soft/hard-iron calibration for tight north lock.","sys.sensors.jsonToggle":"Raw JSON (troubleshooting)","sys.hmi.title":"HMI · SPI display","sys.hmi.desc":"ST7796 via hardware plane (default 320×320). Requires OGSCOPE_DISPLAY_ENABLED=true and SPI enabled (/dev/spidev0.0).","sys.hmi.status.section":"Display & hardware plane","sys.hmi.status.refresh":"Refresh","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"present","sys.hmi.status.no":"missing","sys.hmi.status.resolution":"Resolution / DC","sys.hmi.status.driver":"Driver handle","sys.hmi.status.open":"open","sys.hmi.status.closed":"closed","sys.hmi.status.screenOutput":"Logical output","sys.hmi.status.on":"on","sys.hmi.status.off":"off","sys.hmi.status.lastPattern":"Last pattern","sys.hmi.status.lastError":"Last error","sys.hmi.actions.section":"Output to panel","sys.hmi.actions.hint":"Uses hardware plane device.command → hmi; large frames use a longer RPC timeout.","sys.hmi.actions.smoke":"Test pattern (text + frame)","sys.hmi.actions.colorbars":"Color bars","sys.hmi.actions.fill":"Fill RGB","sys.hmi.actions.screenOn":"Enable panel output","sys.hmi.actions.screenOff":"Pause panel output","sys.hmi.actions.release":"Release driver (close SPI/GPIO)","sys.hmi.rawJson":"Raw JSON response","sys.placeholder.hmi.title":"HMI Console","sys.placeholder.hmi.desc":"Debug workspace for SPI display, key matrix, backlight, and UI replay.","sys.placeholder.hmi.block1":"Display preview","sys.placeholder.hmi.block2":"Input event stream","sys.placeholder.hmi.block3":"Mode controls","sys.placeholder.power.title":"Power Console","sys.placeholder.power.desc":"Debug workspace for battery, charging, power profile, and thermal policy.","sys.placeholder.power.block1":"Power dashboard","sys.placeholder.power.block2":"Power policy table","sys.placeholder.power.block3":"Recovery actions","cam.title":"Camera Debug Console","cam.subtitle":"Migrated to unified admin UI with key optimizations preserved","cam.btn.system":"System Admin","cam.btn.analysis":"Analysis Console","cam.btn.home":"Home","cam.btn.start":"Start Preview","cam.btn.starting":"Starting...","cam.btn.stop":"Stop Preview","cam.btn.stopping":"Stopping...","cam.btn.capture":"Capture","cam.btn.capturing":"Capturing...","cam.btn.recordStart":"Start Recording","cam.btn.recordStop":"Stop Recording","cam.btn.recordBusy":"Switching recording...","cam.btn.refresh":"Refresh Status","cam.preview.title":"Live Preview","cam.preview.state":"State","cam.preview.mode":"Exposure Mode","cam.preview.emptyTitle":"Preview is not started","cam.preview.emptyDesc":"Start the camera preview stream with the button below.","cam.focus.title":"Star Focus Calibration","cam.focus.intro":"Measures focus from multiple stars in raw frames. Rotate the lens slowly after starting; lower HFD is sharper, and the yellow ring marks the reference star.","cam.focus.start":"Start Focus Calibration","cam.focus.starting":"Preparing camera…","cam.focus.finish":"Finish Calibration","cam.focus.resetBest":"Reset Session Best","cam.focus.autoSelect":"Resume Auto Selection","cam.focus.active":"Measuring","cam.focus.idle":"Not started","cam.focus.currentHfd.multi":"Current Multi-star HFD","cam.focus.currentHfd.single":"Current Locked-star HFD","cam.focus.bestHfd":"Session Best","cam.focus.lowerBetter":"lower is better","cam.focus.stars":"Usable / Detected Stars","cam.focus.fwhm":"Median FWHM","cam.focus.concentration":"Core Concentration","cam.focus.selectedStar":"Reference Star","cam.focus.stability":"In-frame Spread","cam.focus.history":"Recent Focus Trend","cam.focus.historyEmpty":"Collecting focus samples…","cam.focus.clickHint":"Click the preview to lock a star","cam.focus.autoTarget":"Automatically selecting a high-SNR star","cam.focus.manualTarget":"Locked near the clicked position","cam.focus.confidence.high":"High confidence","cam.focus.confidence.medium":"Medium confidence","cam.focus.confidence.low":"Low confidence; find more stars","cam.focus.guidance.collecting":"Keep the device still while a baseline is collected…","cam.focus.guidance.noStars":"No usable stars found. Check sky direction, exposure, and the lens cap.","cam.focus.guidance.best":"Near the session best. Fine-tune, hold for a few seconds, then lock the lens when stable.","cam.focus.guidance.improving":"Focus is improving. Continue with a small turn in the same direction.","cam.focus.guidance.worsening":"Focus is getting worse. Reverse slightly and return to the session best.","cam.focus.guidance.steady":"The change is small. Use a finer adjustment and watch for a lower session best.","cam.focus.warning.saturated":"Some bright stars were saturated and excluded. Lower exposure or gain if too few stars remain.","cam.focus.warning.targetNotFound":"No usable star was found near that point. Click closer to a star center or resume automatic selection.","cam.focus.step.start":"Point at the sky, start calibration, and wait for at least three usable stars.","cam.focus.step.adjust":"Rotate the lens slowly and wait for the curve to settle after each adjustment.","cam.focus.step.lock":"Return to the lowest HFD, confirm stability, and lock the lens.","cam.state.streaming":"streaming","cam.state.idle":"idle","cam.state.rec":"REC","cam.stats.requestFps":"Request FPS","cam.hint.mjpegSingleStream":"MJPEG concurrent streams are limited (default ~2). Beyond the cap, extra tabs may contend or get busy—reduce simultaneous live previews.","cam.stats.frameFps":"Display FPS","cam.stats.fpsMeasureNote":"Pixel deltas on a 32×32 sample over ~1s. Multiple previews, slow links, or a stream cap below the sensor target often read much lower.","cam.err.streamBusy":"Preview unavailable: MJPEG slot is already in use","cam.err.streamBusyHint":"Close the live preview in other tabs or in the Analysis console, then try again.","cam.err.streamProbeFailed":"Could not open the preview stream","cam.err.streamProbeDetail":"Check network and camera service, or retry in a moment.","cam.stats.targetFps":"Camera capture target FPS","cam.stats.streamPacingFps":"Shared stream / MJPEG cap FPS","cam.stats.streamPacingHint":"Matches OGSCOPE_SHARED_PREVIEW_FPS: shared grabber pacing and per-stream MJPEG min frame spacing.","cam.stats.captureFps":"Actual capture FPS","cam.stats.encodeMs":"Average JPEG encode","cam.stats.encoder":"Preview encoder","cam.stats.driver":"Camera driver","cam.stats.metadata":"Metadata","cam.stats.consumers":"Preview/analysis/record consumers","cam.stats.cameraMemory":"Process memory/swap","cam.stats.longExposureThrottle":"Auto exposure is about {exposure} ms, so long exposure is limiting FPS; exposure is not shortened to preserve analysis quality.","cam.stats.uptime":"Stream Uptime","cam.controls.title":"Controls","cam.controls.core":"Core Settings","cam.controls.fps":"FPS","cam.controls.sensorFps":"Sensor target FPS","cam.controls.previewFps":"Preview target FPS","cam.controls.resolution":"Resolution","cam.controls.applyRes":"Apply resolution","cam.controls.sampling":"Sampling mode","cam.controls.exposure":"Exposure","cam.controls.gain":"Analog Gain","cam.controls.digitalGain":"Digital Gain","cam.controls.noiseReduction":"Noise Reduction","cam.controls.noiseReductionMode":"Noise Reduction","cam.controls.digitalGainReadOnly":"Digital gain is read-only on this driver","cam.controls.aeFlicker":"AE Flicker","cam.controls.maxAeFrame":"Max AE frame (us)","cam.controls.contrast":"Contrast","cam.controls.brightness":"Brightness","cam.controls.saturation":"Saturation","cam.controls.sharpness":"Sharpness","cam.controls.applySettings":"Apply All Settings","cam.controls.applyAll":"Save Parameters","cam.controls.applyRuntime":"Save Runtime Settings","cam.controls.mode":"Mode Controls","cam.controls.autoExposure":"Exposure Mode","cam.controls.whiteBalance":"White Balance","cam.controls.colorMode":"Color Mode","cam.controls.auto":"Auto","cam.controls.manual":"Manual","cam.controls.night":"Night","cam.controls.off":"Off","cam.controls.nr.off":"Off","cam.controls.nr.fast":"Fast","cam.controls.nr.high_quality":"High quality","cam.controls.wb.auto":"Auto","cam.controls.wb.daylight":"Daylight","cam.controls.wb.cloudy":"Cloudy","cam.controls.wb.tungsten":"Tungsten","cam.controls.wb.fluorescent":"Fluorescent","cam.controls.wb.indoor":"Indoor","cam.controls.wb.manual":"Manual","cam.controls.wb.night":"Night","cam.controls.color":"Color","cam.controls.mono":"Mono","cam.controls.applyAe":"Apply Exposure Mode","cam.controls.applyWb":"Apply White Balance","cam.controls.applyColor":"Apply Color Mode","cam.controls.applyMode":"Apply Mode Settings","cam.controls.lockedByAe":"Exposure and gain are locked while auto exposure is enabled.","cam.controls.lockedByWb":"R/B gains are locked unless white balance is in manual mode.","cam.controls.pendingChanges":"You have unapplied parameter changes","cam.controls.tools":"Tools","cam.controls.nightPreset":"Night Preset","cam.controls.nightOn":"Enable Night Mode","cam.controls.nightOff":"Disable Night Mode","cam.controls.backup":"Backup Settings","cam.controls.restore":"Restore Settings","cam.controls.reset":"Reset Camera","cam.quick.title":"Quick Presets & Smart Tuning","cam.quick.daylight":"Daylight","cam.quick.night":"Night","cam.quick.nightPreset":"Apply Night Preset","cam.quick.deep-sky":"Deep Sky","cam.quick.planetary":"Planetary","cam.quick.autoAdjust":"Auto Adjust","cam.quick.nightHint":"Night preset is one-click tuning; night mode toggle is runtime on/off control.","cam.system.title":"System Monitor","cam.system.status":"Camera Ready","cam.system.stream":"Stream","cam.system.sensor":"Sensor","cam.system.quality":"Image Quality","cam.presets.title":"Preset Management","cam.presets.name":"Preset Name","cam.presets.desc":"Preset Description","cam.presets.save":"Save Preset","cam.presets.empty":"No presets","cam.presets.noDesc":"No description","cam.presets.apply":"Apply","cam.presets.delete":"Delete","cam.files.title":"File Management","cam.files.refresh":"Refresh File List","cam.files.empty":"No files","cam.files.download":"Download","cam.files.info":"Details","cam.files.delete":"Delete","cam.files.loadingInfo":"Loading file details...","cam.files.prev":"Prev","cam.files.next":"Next","cam.files.page":"Page {current}/{total}","cam.files.closeDetail":"Close details","cam.files.size":"Size","cam.files.type":"Type","cam.files.modified":"Modified","cam.hist.title":"Histogram","cam.hist.enabled":"Enable histogram","cam.hist.luminance":"Luminance","cam.hist.over":"Over-exposure","cam.hist.expand":"Expand Histogram","cam.hist.collapse":"Collapse Histogram","cam.notice.previewStart":"Preview started","cam.notice.previewStop":"Preview stopped","cam.notice.captureSaved":"Capture saved: {name}","cam.notice.recordStart":"Recording started: {name}","cam.notice.recordStop":"Recording stopped","cam.notice.fpsApplied":"FPS updated to {fps}","cam.notice.resApplied":"Resolution updated to {w}x{h}","cam.notice.samplingApplied":"Sampling mode updated: {mode}","cam.notice.settingsApplied":"Settings applied","cam.notice.aeApplied":"Exposure mode applied","cam.notice.wbApplied":"White balance applied","cam.notice.colorApplied":"Color mode applied","cam.notice.modeApplied":"Mode settings applied","cam.notice.runtimeApplied":"Runtime settings applied","cam.notice.rotationApplied":"Rotation set to {value}°","cam.mirror.hint":"Use with rotation when the camera is mounted inverted; preview, plate solve, and polar guide share one coordinate frame.","cam.mirror.horizontal":"Horizontal mirror","cam.mirror.vertical":"Vertical mirror","cam.notice.mirrorApplied":"Mirror settings updated","cam.notice.nightOn":"Night mode enabled","cam.notice.nightOff":"Night mode disabled","cam.notice.nightPreset":"Night preset applied","cam.notice.reset":"Camera settings reset","cam.notice.backup":"Settings backed up","cam.notice.restore":"Settings restored","cam.notice.presetSaved":"Preset saved: {name}","cam.notice.presetApplied":"Preset applied: {name}","cam.notice.presetDeleted":"Preset deleted: {name}","cam.notice.download":"Download started: {name}","cam.notice.downloadWithSidecar":"Downloaded: {name} (with sidecar {sidecar})","cam.notice.fileDeleted":"File deleted: {name}","cam.notice.quickPresetApplied":"Quick preset applied: {preset}","cam.notice.needPreview":"Start preview first","cam.notice.needManualForAutoAdjust":"Switch to manual exposure before auto-adjust","cam.notice.autoAdjusted":"Auto-adjust completed and applied","cam.confirm.reset":"Reset camera settings on the current device?","cam.confirm.deletePreset":"Delete preset {name}?","cam.confirm.deleteFile":"Delete file {name}?","sys.sensors.mag.calStatusPrefix":"Calibration mode:","sys.sensors.mag.calModeAuto":"Auto (not locked)","sys.sensors.mag.calModeRecording":"Recording (rotate device)","sys.sensors.mag.calModeLocked":"Locked (saved heading)","sys.sensors.mag.calSamplesPrefix":"Recorded samples:","sys.sensors.mag.calRecordingHint":"Tip: keep level and rotate slowly; save after 10+ samples.","sys.sensors.mag.calLockedHint":"Locked heading params:"},C={"app.title":"OGScope 星空解算控制台","nav.lab":"解算台","nav.labImage":"图片解算","nav.labVideo":"视频解算","nav.pool":"素材池","nav.history":"实验记录","nav.cameraDebug":"相机调试控制台","nav.home":"首页","nav.systemAdmin":"返回系统后台","lang.zh":"中文","lang.en":"EN","sidebar.assets":"自行上传素材","sidebar.upload":"上传文件","sidebar.refresh":"刷新列表","sidebar.debugCaptures":"调试控制台素材","sidebar.assetTypeImage":"图片","sidebar.assetTypeVideo":"视频","sidebar.debugEmpty":"暂无调试文件","sidebar.importToPool":"导入到素材池","sidebar.importToPoolWithTranscode":"转码并导入素材池","sidebar.importToPoolDirect":"直接导入素材池","sidebar.flowPreparing":"准备中…","sidebar.flowImportingDebug":"正在导入调试素材…","sidebar.flowDownloadingPool":"正在下载已导入文件…","sidebar.flowWritingBuffer":"正在写入转码缓冲区…","sidebar.flowLoadingTranscoder":"正在加载转码器…","sidebar.flowTranscoding":"正在转码 AVI -> MP4…","sidebar.flowPackaging":"正在封装转码结果…","sidebar.flowUploadingMp4":"正在上传 MP4…","sidebar.flowReplacing":"正在替换并清理原 AVI…","sidebar.flowNoTranscode":"该文件无需转码,已直接导入。","sidebar.flowDone":"流程完成","sidebar.flowDoneMsg":"已完成:{name}","sidebar.flowFailed":"流程失败","sidebar.debugPage":"第 {cur} / {total} 页","sidebar.batchPresets":"批量预设","sidebar.batchHint":"勾选后点击「批量解算」可一次用多组参数对比结果。","lab.selectOrUpload":"从左侧选择自行上传或已导入的素材","lab.selectOrUploadVideo":"从左侧选择视频素材预览;或使用上方按钮解算设备相机实时帧。","lab.file":"文件","lab.source":"来源","lab.layers":"叠加层","lab.layer.matched":"匹配星","lab.layer.pattern":"图案星","lab.layer.all":"全部质心","lab.layer.rejected":"已剔除(红叉)","lab.previewConfidence":"置信度","lab.grid":"网格","lab.zoomIn":"放大","lab.zoomOut":"缩小","lab.zoomReset":"复位","lab.resolution":"分辨率","lab.fwhm":"FWHM","lab.starsDetected":"检测星点","lab.meta.title":"拍摄与文件信息","lab.meta.noSidecar":"无侧车信息(非调试采集或仅本地上传)","lab.meta.partial":"暂无侧车详细字段,仅显示文件信息。","lab.solveSection":"解算","lab.imageSection":"图像","lab.metric.solveMs":"用时","lab.metric.solveComputeMs":"解算计算用时","lab.metric.solveComputeHelp":"服务端 Tetra3 与提星等纯计算耗时(与网络无关)。","lab.metric.solveRoundTripMs":"全链路用时","lab.metric.solveRoundTripHelp":"从本页发起请求到收到结果并完成界面刷新的总耗时,含网络往返与浏览器渲染。","lab.metric.backendTotalMs":"后端总用时","lab.metric.openDecodeMs":"读取/解码","lab.metric.preprocessMs":"预处理","lab.metric.extractMs":"提星","lab.metric.solveOnlyMs":"匹配解算","lab.metric.probHelp":"Tetra3 的 Prob 为假阳性概率(越低越可信)。一般使用 (1−Prob)×100%;当 Prob 极小时浮点数会舍入为 100%,此时用 −log₁₀(Prob) 映射到约 70–100% 以便区分。","lab.metric.probRawHelp":"Tetra3 返回的原始 Prob 字段,可能为对数似然等内部量;与上一行换算后的百分比对照查看即可。","lab.metric.radec":"RA / Dec","lab.metric.matches":"匹配","lab.metric.rmse":"RMSE","lab.metric.prob":"置信","lab.metric.status":"状态","lab.gateNextMs":"约 {ms} ms 后可重试","lab.gate.skipRecording":"录制中:相机正写入录像,暂无法从相机取帧解算;请先停止录制后再试。","lab.gate.skipBusyInFlight":"上一帧解算仍在处理中,请稍候。若频繁出现,可适当增大「实时解算间隔」。","lab.gate.skipInterval":"解算请求过于频繁,已按后端节流排队。","lab.gate.skipBusyFallback":"实时解算暂时无法开始({detail})。","lab.centroidQualityTitle":"质心质量","lab.centroidQualityMetrics":"输入 {in} → 保留 {out};剔除过密 {dense},共线 {line}","meta.exposure":"曝光","meta.gain":"增益","meta.fps":"帧率","meta.sensor":"传感器","meta.colorMode":"色彩","meta.outputResolution":"输出分辨率","meta.fileTime":"文件时间","meta.fileSize":"文件大小","results.viewRaw":"原始 JSON","results.hideRaw":"收起","results.solveHistoryTitle":"解算历史","results.historySingle":"单帧","results.historyBatch":"批量 {count} 组","params.title":"解算参数","params.blockSolveIntro":"以下为板块求解(Tetra3)搜索天区、超时与粗略指向提示;FOV 需与镜头视场大致一致。","params.centroid":"提星","params.blockCentroidIntro":"以下为星点检测:阈值、连通域面积与局部背景窗口,用于从图像中提取星点质心。","params.fov":"FOV 估计 (°)","params.fovHelp":"水平视场角估计值,用于 lost-in-space 解算。","params.fovErr":"FOV 允许误差 (°)","params.fovErrHelp":"允许 Tetra3 在估计 FOV 附近的搜索范围。","params.timeout":"超时 (ms)","params.timeoutHelp":"单次解算最长等待时间。","params.solveIntervalMs":"实时解算间隔 (ms)","params.solveIntervalMsHelp":"期望间隔可调整,但会被后端限制在安全范围内。","params.solveIntervalIndependent":"解算间隔独立于相机采集和预览帧率。","params.solveIntervalBound":"后端限制范围:{min}-{max} ms,当前生效:{effective} ms","params.centroidRejectionLevel":"质心剔除强度 (1–5)","params.centroidRejectionLevelHelp":"1 最保守保留更多星点,5 更激进剔除过密与共线假星;默认 3。","params.centroidRejectionScale":"保守 ← → 激进","params.solveProfile":"解算档位","params.solveProfileHelp":"速度/平衡/稳健三档会同时调整超时、提星阈值与参与匹配星点数。","params.solveProfileSpeed":"速度优先","params.solveProfileBalanced":"平衡","params.solveProfileRobust":"稳健优先","params.ra":"RA 提示 (°)","params.raHelp":"大致天球赤经,缩小搜索范围(度)。","params.dec":"Dec 提示 (°)","params.decHelp":"大致天球赤纬(度)。","params.maxSide":"提星前长边上界 (px)","params.maxSideHelp":"降采样长边上限,大图可加速提星。","params.detailLevelFull":"包含完整 Tetra3 原始结果(体积略大,仅调试时开启)","params.largeScaleBg":"大尺度背景减除","params.largeScaleBgHelp":"在提星前用低分辨率平滑估计并校正大尺度亮度不均,可减轻角部光晕导致的假星;默认关闭以保持与过往行为一致。","params.sigma":"σ(阈值倍数)","params.sigmaHelp":"高于背景噪声倍数的区域视为星点候选。","params.maxArea":"max_area","params.maxAreaHelp":"连通域最大像素面积。","params.minArea":"min_area","params.minAreaHelp":"连通域最小像素面积。","params.filtsize":"filtsize(奇数)","params.filtsizeHelp":"局部背景滤波窗口边长,须为奇数。","btn.solveOne":"单张解算","btn.solveBatch":"批量解算(勾选预设)","btn.applyPresets":"应用预设到表单","btn.savePreset":"保存","placeholder.newPreset":"新预设名称","pool.title":"服务器素材池","pool.col.name":"文件名","pool.col.source":"来源","pool.col.size":"大小","pool.col.time":"修改时间","pool.delete":"删除","history.title":"实验记录","history.intro":"此处展示你在解算台完成解算后手动保存的快照。用法:在「解算台」主栏「结果对比」中,单张解算后点「保存当前到实验记录」,或批量解算后在某张结果卡片上点「保存记录」。本页可按文件名或预设名搜索,支持导出 JSON/CSV 备份。","history.search":"搜索…","history.searchBtn":"搜索","history.exportJson":"导出 JSON","history.exportCsv":"导出 CSV","history.total":"共 {n} 条","history.preset":"预设","history.metrics":"指标","history.detail":"详情","history.collapse":"收起","history.prev":"上一页","history.next":"下一页","history.delete":"删除","delete.uploadFirst":"确定要从素材池删除「{name}」吗?","delete.uploadSecond":"此操作不可恢复,再次确认删除?","delete.experimentFirst":"确定要删除这条实验记录吗?","delete.experimentSecond":"此操作不可恢复,再次确认删除?","results.title":"结果对比","results.saveCurrent":"保存当前到实验记录","results.saveRow":"保存记录","results.expand":"展开","results.collapseJson":"收起","err.selectFile":"请选择素材","err.selectPresets":"请勾选至少一个预设","common.placeholder":"—","delete.uploadCascade":"该素材有 {n} 条实验记录,是否一并删除?","lab.solveCurrentFrame":"解算当前帧(文件)","lab.solveFileStart":"开始文件连续解算","lab.solveFileStop":"停止文件连续解算","lab.cameraPreviewLoading":"正在连接共享预览…","lab.solveCameraFrame":"解算相机当前帧","lab.solveCameraStart":"开始相机解算","lab.solveCameraStop":"停止相机解算","lab.videoPreviewFailed":"该视频格式可能不受浏览器支持,建议使用 MP4(H.264) 或 WebM。","lab.previewModeFile":"素材文件","lab.previewModeCamera":"设备相机","lab.stopCameraPreview":"停止预览","lab.videoLiveIntro":"与调试控制台共用同一相机;此处可预览并解算实时帧,无需单独打开调试页。两页可同时使用。","lab.videoMjpegHint":"MJPEG 同时连接数已满:请关闭其它页面的实时预览或稍后重试(默认允许约 4 路,可在 systemd 调整 OGSCOPE_STREAM_MAX_MJPEG_CLIENTS)。","lab.cameraStreamStatusFail":"无法读取视频流名额状态,请稍后重试。","lab.cameraSnapshotName":"ogscope_camera_live","lab.metric.probRaw":"原始 Prob","lab.systemLoad":"系统负载","results.saveBatchAll":"保存全部到实验记录","lab.transcode.title":"AVI 文件需先转码","lab.transcode.desc":"当前视频为 AVI。为保证浏览器可预览与连续解算,请先在本地转码为 MP4 并上传替换。上传成功后将自动删除服务器上的原 AVI。","lab.transcode.button":"转码并上传替换","lab.transcode.loading":"正在加载转码器…","lab.transcode.writingBuffer":"正在写入转码缓冲区…","lab.transcode.running":"正在转码 AVI -> MP4…","lab.transcode.packaging":"正在封装转码结果…","lab.transcode.uploading":"正在上传转码结果…","lab.transcode.done":"转码并替换完成,可继续预览与解算。","lab.transcode.failed":"转码或上传失败,请重试。","lab.transcode.fetchFailed":"无法读取服务器上的 AVI 文件。","sys.shell.subtitle":"系统调试控制台","sys.shell.nav.overview":"总览","sys.shell.nav.network":"网络","sys.shell.nav.camera":"相机调试","sys.shell.nav.analysis":"寻星控制台","sys.shell.nav.sensors":"传感器","sys.shell.nav.power":"电源","sys.shell.nav.hmi":"人机交互","sys.shell.nav.config":"配置管理","sys.shell.workbench":"WORKBENCH / 系统工作台","sys.shell.node":"节点","sys.shell.top.overview":"系统状态","sys.shell.top.network":"网络与 WiFi","sys.shell.top.sensors":"传感器","sys.shell.top.power":"电源管理","sys.shell.top.hmi":"人机交互","sys.shell.top.config":"配置管理","sys.overview.breadcrumb.console":"控制台","sys.overview.breadcrumb.module":"系统总览","sys.overview.title":"系统总览","sys.overview.subtitle":"实时系统运行健康状况","sys.overview.metric.cpu":"CPU 使用率","sys.overview.metric.mem":"内存占用","sys.overview.metric.temp":"核心温度","sys.overview.metric.wifi":"WiFi 链路","sys.overview.metric.uptime":"运行时长","sys.overview.metric.load":"1 分钟负载","sys.overview.metric.storage":"存储","sys.overview.linkActive":"链路在线","sys.overview.tempState":"状态: 正常","sys.overview.wifiSummary":"无线网络摘要","sys.overview.iface":"接口","sys.overview.signal":"信号","sys.overview.quality":"质量","sys.overview.storageComingSoon":"即将接入","sys.logs.title":"系统日志","sys.logs.kernel":"内核","sys.logs.liveToggle":"实时拉取","sys.logs.refresh":"刷新","sys.logs.liveOffHint":"实时拉取关闭(默认)","sys.logs.loading":"日志加载中...","sys.logs.empty":"暂无日志","sys.placeholder.breadcrumb":"控制台 / 预留模块","sys.placeholder.block":"预留模块","sys.placeholder.desc":"模块壳层已预留,后续将按统一组件体系接入真实数据。","sys.placeholder.status":"STATUS: RESERVED / 当前为结构化占位页面。","sys.placeholder.sensors.title":"传感器诊断台","sys.placeholder.sensors.desc":"用于 IMU、温湿度、电流电压、姿态与时序采样的统一监控面板。","sys.placeholder.sensors.block1":"实时曲线区","sys.placeholder.sensors.block2":"设备健康卡","sys.placeholder.sensors.block3":"阈值告警区","sys.sensors.title":"传感器诊断台","sys.sensors.desc":"磁力计罗盘指北与 MPU-6050 陀螺仪角速度可视化;底层仍为开发者调试 API。","sys.sensors.mag.section":"磁力计(AK09911 / I²C)","sys.sensors.mag.note":"默认总线 1、地址 12(0x0C,CAD 接 GND)。无需在应用层“定义 GPIO”;请确保固件已启用 I²C 且已加载 i2c-dev。","sys.sensors.mag.bus":"I²C 总线号","sys.sensors.mag.addr":"7-bit 地址(十进制,12=0x0C)","sys.sensors.mag.i2cdetect":"附带运行 i2cdetect","sys.sensors.mag.btnSelftest":"运行自检","sys.sensors.mag.btnProbe":"扫描各总线","sys.sensors.mag.btnCalStart":"开始方向校准","sys.sensors.mag.btnCalCommit":"保存并锁定方向","sys.sensors.mag.btnCalReset":"重置到自动模式","sys.sensors.mag.btnCalStatus":"查看校准状态","sys.sensors.mag.running":"执行中…","sys.sensors.mpu.section":"IMU(MPU-6050 / I²C)","sys.sensors.mpu.note":"默认总线 1、地址 104(0x68,AD0 接 GND)。含陀螺仪角速度采样(MPU 内集成加速度计与陀螺仪)。","sys.sensors.mpu.addr":"7-bit 地址(十进制,104=0x68)","sys.sensors.mpu.btnSelftest":"运行 MPU 自检","sys.sensors.mpu.running":"执行中…","sys.sensors.gyro.title":"陀螺仪(MPU-6050)","sys.sensors.gyro.subtitle":"使用上方同一总线/地址;一次读取加速度与陀螺仪,估算横滚/俯仰并显示 3D 板卡示意。","sys.sensors.gyro.btn":"读取角速度","sys.sensors.gyro.loading":"采样中…","sys.sensors.gyro.dps":"角速度","sys.sensors.gyro.raw":"原始值","sys.sensors.gyro.unitDps":"°/s,±250°/s 量程、默认灵敏度","sys.sensors.gyro.unitRaw":"16-bit 有符号","sys.sensors.gyro.hint":"点击「读取角速度」从 IMU 采样(加速度 0x3B + 陀螺仪 0x43)。横滚/俯仰来自重力;ωz 为绕竖直轴角速度,并非绝对航向。","sys.sensors.gyro.errUnknown":"采样失败","sys.sensors.gyro.live":"连续刷新(约 2 Hz)","sys.sensors.gyro.barsTitle":"角速度条(相对 ±250°/s 满偏)","sys.sensors.gyro.rawBlock":"寄存器原始值(16-bit)","sys.sensors.gyro.att3dTitle":"3D 姿态示意","sys.sensors.gyro.att3dDesc":"横滚/俯仰由加速度计重力估算;ωz 为陀螺仪绕竖直轴角速度(非磁航向)。机体系三轴与板卡固连,随姿态在三维空间中转动。","sys.sensors.gyro.axisXLabel":"+X","sys.sensors.gyro.axisYLabel":"+Y","sys.sensors.gyro.axisZLabel":"+Z","sys.sensors.gyro.att3dBodyAxes":"左侧为与电路板固连的机体系:琥珀 +X、天蓝 +Y、紫 +Z(法向);短划为负向参考,整体随横滚/俯仰旋转。","sys.sensors.gyro.att3dRefTitle":"正交轴参考(固定视角)","sys.sensors.gyro.att3dRefDesc":"与左侧同色对应,便于辨认三轴空间关系。","sys.sensors.gyro.roll":"横滚","sys.sensors.gyro.pitch":"俯仰","sys.sensors.gyro.yawRate":"偏航角速度","sys.sensors.compass.tapeCaption":"水平航向带 · 中心为当前读数","sys.sensors.compass.title":"磁力计 · 指北罗盘","sys.sensors.compass.desc":"根据水平面磁场分量估算方向角,用于调试「是否大致指向北方」;精对准需校准与固定安装姿态。","sys.sensors.compass.btn":"刷新罗盘","sys.sensors.compass.loading":"测量中…","sys.sensors.compass.live":"连续刷新","sys.sensors.compass.headingLabel":"水平航向角","sys.sensors.compass.headingHint":"由 atan2(Hx, Hy) 得到,单位度","sys.sensors.compass.cardN":"北","sys.sensors.compass.cardE":"东","sys.sensors.compass.cardS":"南","sys.sensors.compass.cardW":"西","sys.sensors.compass.err":"采样失败","sys.sensors.compass.footnote":"红色针尖表示 XY 平面内磁场水平分量方向;转动设备时指针应随之变化。精对准需软铁/硬铁校准。","sys.sensors.jsonToggle":"展开原始 JSON(排障用)","sys.hmi.title":"人机交互 · SPI 显示","sys.hmi.desc":"通过硬件平面驱动 ST7796(默认 320×320);需 OGSCOPE_DISPLAY_ENABLED=true 且系统已启用 SPI(/dev/spidev0.0)。","sys.hmi.status.section":"显示与硬件平面状态","sys.hmi.status.refresh":"刷新状态","sys.hmi.status.displayEnabled":"DISPLAY_ENABLED","sys.hmi.status.spidev":"/dev/spidev0.0","sys.hmi.status.yes":"存在","sys.hmi.status.no":"不存在","sys.hmi.status.resolution":"分辨率 / DC","sys.hmi.status.driver":"驱动句柄","sys.hmi.status.open":"已打开","sys.hmi.status.closed":"未打开","sys.hmi.status.screenOutput":"逻辑输出","sys.hmi.status.on":"开","sys.hmi.status.off":"关","sys.hmi.status.lastPattern":"上次图案","sys.hmi.status.lastError":"上次错误","sys.hmi.actions.section":"输出到屏幕","sys.hmi.actions.hint":"以下为硬件平面 device.command → hmi;大屏刷新使用较长 RPC 超时。","sys.hmi.actions.smoke":"测试画面(文字+边框)","sys.hmi.actions.colorbars":"彩条","sys.hmi.actions.fill":"填充 RGB","sys.hmi.actions.screenOn":"允许屏幕输出","sys.hmi.actions.screenOff":"暂停屏幕输出","sys.hmi.actions.release":"释放驱动(关闭 SPI/GPIO)","sys.hmi.rawJson":"原始 JSON 响应","sys.placeholder.hmi.title":"人机交互台","sys.placeholder.hmi.desc":"用于 SPI 显示、按键矩阵、背光/对比度与界面回放的调试工作区。","sys.placeholder.hmi.block1":"显示预览区","sys.placeholder.hmi.block2":"输入事件流","sys.placeholder.hmi.block3":"参数与模式控制","sys.placeholder.power.title":"电源与功耗台","sys.placeholder.power.desc":"用于电池、充电、瞬时功耗与热管理策略联动调试。","sys.placeholder.power.block1":"功耗看板","sys.placeholder.power.block2":"电源策略表","sys.placeholder.power.block3":"恢复与保护动作","cam.title":"相机调试控制台","cam.subtitle":"迁移到统一后台架构,保留关键性能优化","cam.btn.system":"系统后台","cam.btn.analysis":"寻星控制台","cam.btn.home":"首页","cam.btn.start":"启动预览","cam.btn.starting":"启动中...","cam.btn.stop":"停止预览","cam.btn.stopping":"停止中...","cam.btn.capture":"拍摄","cam.btn.capturing":"拍摄中...","cam.btn.recordStart":"开始录制","cam.btn.recordStop":"停止录制","cam.btn.recordBusy":"录制切换中...","cam.btn.refresh":"刷新状态","cam.preview.title":"实时预览","cam.preview.state":"状态","cam.preview.mode":"曝光模式","cam.preview.emptyTitle":"预览尚未启动","cam.preview.emptyDesc":"点击下方按钮启动相机预览流。","cam.focus.title":"星点焦点校准","cam.focus.intro":"用原始帧中的多颗星测量焦点质量。开始后缓慢旋转镜头;HFD 越小越清晰,黄色圆圈表示当前参考星。","cam.focus.start":"开始焦点校准","cam.focus.starting":"准备相机…","cam.focus.finish":"完成校准","cam.focus.resetBest":"重置本轮最佳","cam.focus.autoSelect":"恢复自动选星","cam.focus.active":"测量中","cam.focus.idle":"未开始","cam.focus.currentHfd.multi":"当前多星 HFD","cam.focus.currentHfd.single":"当前锁定星 HFD","cam.focus.bestHfd":"本轮最佳","cam.focus.lowerBetter":"越低越好","cam.focus.stars":"可用星 / 检测星","cam.focus.fwhm":"中位 FWHM","cam.focus.concentration":"核心能量集中度","cam.focus.selectedStar":"参考星","cam.focus.stability":"帧内离散度","cam.focus.history":"最近焦点趋势","cam.focus.historyEmpty":"正在收集焦点样本…","cam.focus.clickHint":"可点击预览锁定一颗星","cam.focus.autoTarget":"自动选择高信噪比星点","cam.focus.manualTarget":"已按点击位置锁定","cam.focus.confidence.high":"高置信度","cam.focus.confidence.medium":"中等置信度","cam.focus.confidence.low":"低置信度,请寻找更多星点","cam.focus.guidance.collecting":"保持设备稳定,正在建立基线…","cam.focus.guidance.noStars":"没有找到可用星点:请检查天空方向、曝光和镜头保护盖。","cam.focus.guidance.best":"已接近本轮最佳;微调并保持数秒,确认读数稳定后锁紧镜头。","cam.focus.guidance.improving":"正在变清晰,继续沿当前方向小幅旋转。","cam.focus.guidance.worsening":"清晰度变差,建议反向微调并回到本轮最佳。","cam.focus.guidance.steady":"变化很小;请用更小角度微调,观察本轮最佳是否下降。","cam.focus.warning.saturated":"部分亮星已饱和并被排除;若可用星过少,请降低曝光或增益。","cam.focus.warning.targetNotFound":"点击位置附近没有可用星点,请点击更靠近星点中心的位置,或恢复自动选星。","cam.focus.step.start":"对准星空并开始校准,等待至少三颗可用星。","cam.focus.step.adjust":"缓慢旋转镜头,每次调整后等待曲线稳定。","cam.focus.step.lock":"回到本轮最低 HFD,保持稳定后锁紧镜头。","cam.state.streaming":"流已启动","cam.state.idle":"未启动","cam.state.rec":"录制中","cam.stats.requestFps":"请求 FPS","cam.hint.mjpegSingleStream":"MJPEG 同时连接数有限(默认约两路);超出上限时多标签页会争用或返回忙,请减少同时打开的实时预览。","cam.stats.frameFps":"画面 FPS","cam.stats.fpsMeasureNote":"统计最近约 1 秒内在 32×32 采样上的像素变化次数;多路同时预览、网络较慢、或下方「共享流上限」低于相机目标时,读数常明显偏低。","cam.err.streamBusy":"无法连接预览:MJPEG 名额已被占用","cam.err.streamBusyHint":"请关闭其它标签页或解算台中的设备实时预览,或稍后再试。","cam.err.streamProbeFailed":"无法建立预览流","cam.err.streamProbeDetail":"请检查网络与相机服务状态,或稍后重试。","cam.stats.targetFps":"相机采集目标 FPS","cam.stats.streamPacingFps":"共享流/MJPEG 上限 FPS","cam.stats.streamPacingHint":"与进程环境 OGSCOPE_SHARED_PREVIEW_FPS 一致,约束共享抓帧节奏与每路 MJPEG 的最小帧间隔。","cam.stats.captureFps":"实际采集 FPS","cam.stats.encodeMs":"JPEG 平均编码","cam.stats.encoder":"预览编码器","cam.stats.driver":"相机驱动","cam.stats.metadata":"元数据","cam.stats.consumers":"预览/分析/录像消费者","cam.stats.cameraMemory":"进程内存/Swap","cam.stats.longExposureThrottle":"自动曝光约 {exposure} ms,实际帧率受长曝光限制;为保证分析效果未强制缩短曝光。","cam.stats.uptime":"流运行时长","cam.controls.title":"参数控制","cam.controls.core":"核心参数","cam.controls.fps":"帧率","cam.controls.sensorFps":"传感器目标 FPS","cam.controls.previewFps":"预览目标 FPS","cam.controls.resolution":"分辨率","cam.controls.applyRes":"应用分辨率","cam.controls.sampling":"采样模式","cam.controls.exposure":"曝光","cam.controls.gain":"模拟增益","cam.controls.digitalGain":"数字增益","cam.controls.noiseReduction":"降噪级别","cam.controls.noiseReductionMode":"降噪模式","cam.controls.digitalGainReadOnly":"当前驱动仅上报数字增益,不支持手动设置","cam.controls.aeFlicker":"AE 防闪烁","cam.controls.maxAeFrame":"最长自动曝光帧周期(us)","cam.controls.contrast":"对比度","cam.controls.brightness":"亮度","cam.controls.saturation":"饱和度","cam.controls.sharpness":"锐度","cam.controls.applySettings":"应用全部设置","cam.controls.applyAll":"保存参数","cam.controls.applyRuntime":"保存运行参数","cam.controls.mode":"模式控制","cam.controls.autoExposure":"曝光模式","cam.controls.whiteBalance":"白平衡","cam.controls.colorMode":"颜色模式","cam.controls.auto":"自动","cam.controls.manual":"手动","cam.controls.night":"夜间","cam.controls.off":"关闭","cam.controls.nr.off":"关闭","cam.controls.nr.fast":"快速","cam.controls.nr.high_quality":"高质量","cam.controls.wb.auto":"自动","cam.controls.wb.daylight":"日光","cam.controls.wb.cloudy":"阴天","cam.controls.wb.tungsten":"钨丝灯","cam.controls.wb.fluorescent":"荧光灯","cam.controls.wb.indoor":"室内","cam.controls.wb.manual":"手动","cam.controls.wb.night":"夜间","cam.controls.color":"彩色","cam.controls.mono":"黑白","cam.controls.applyAe":"应用曝光模式","cam.controls.applyWb":"应用白平衡","cam.controls.applyColor":"应用颜色模式","cam.controls.applyMode":"应用模式设置","cam.controls.lockedByAe":"自动曝光开启时,曝光与增益参数锁定。","cam.controls.lockedByWb":"当前非手动白平衡模式,R/B 增益参数锁定。","cam.controls.pendingChanges":"有未应用的参数修改","cam.controls.tools":"工具","cam.controls.nightPreset":"夜间预设","cam.controls.nightOn":"开启夜间模式","cam.controls.nightOff":"关闭夜间模式","cam.controls.backup":"备份设置","cam.controls.restore":"恢复设置","cam.controls.reset":"重置相机","cam.quick.title":"快速预设与智能调参","cam.quick.daylight":"白天模式","cam.quick.night":"夜间模式","cam.quick.nightPreset":"应用夜间预设","cam.quick.deep-sky":"深空模式","cam.quick.planetary":"行星模式","cam.quick.autoAdjust":"智能调整","cam.quick.nightHint":"夜间预设用于一键切换夜拍参数;夜间模式开关用于实时启停红外/夜景模式。","cam.system.title":"系统监控","cam.system.status":"相机就绪","cam.system.stream":"流状态","cam.system.sensor":"传感器","cam.system.quality":"图像质量","cam.presets.title":"预设管理","cam.presets.name":"预设名称","cam.presets.desc":"预设描述","cam.presets.save":"保存预设","cam.presets.empty":"暂无预设","cam.presets.noDesc":"无描述","cam.presets.apply":"应用","cam.presets.delete":"删除","cam.files.title":"文件管理","cam.files.refresh":"刷新文件列表","cam.files.empty":"暂无文件","cam.files.download":"下载","cam.files.info":"详情","cam.files.delete":"删除","cam.files.loadingInfo":"正在读取文件详情...","cam.files.prev":"上一页","cam.files.next":"下一页","cam.files.page":"第 {current}/{total} 页","cam.files.closeDetail":"关闭详情","cam.files.size":"文件大小","cam.files.type":"文件类型","cam.files.modified":"修改时间","cam.hist.title":"直方图","cam.hist.enabled":"启用直方图","cam.hist.luminance":"亮度通道","cam.hist.over":"过曝警告","cam.hist.expand":"展开直方图","cam.hist.collapse":"收起直方图","cam.notice.previewStart":"预览已启动","cam.notice.previewStop":"预览已停止","cam.notice.captureSaved":"拍摄完成: {name}","cam.notice.recordStart":"开始录制: {name}","cam.notice.recordStop":"录制已停止","cam.notice.fpsApplied":"帧率已设置为 {fps}","cam.notice.resApplied":"分辨率已设置为 {w}x{h}","cam.notice.samplingApplied":"采样模式已设置为 {mode}","cam.notice.settingsApplied":"参数已应用","cam.notice.aeApplied":"曝光模式已应用","cam.notice.wbApplied":"白平衡已应用","cam.notice.colorApplied":"颜色模式已应用","cam.notice.modeApplied":"模式设置已应用","cam.notice.runtimeApplied":"运行参数已应用","cam.notice.rotationApplied":"旋转已设置为 {value}°","cam.mirror.hint":"相机倒装时可配合旋转使用;镜像后预览与星点解算、极轴引导同坐标系。","cam.mirror.horizontal":"水平镜像","cam.mirror.vertical":"垂直镜像","cam.notice.mirrorApplied":"镜像设置已更新","cam.notice.nightOn":"夜间模式已开启","cam.notice.nightOff":"夜间模式已关闭","cam.notice.nightPreset":"夜间预设已应用","cam.notice.reset":"相机设置已重置","cam.notice.backup":"设置已备份","cam.notice.restore":"设置已恢复","cam.notice.presetSaved":"预设已保存: {name}","cam.notice.presetApplied":"预设已应用: {name}","cam.notice.presetDeleted":"预设已删除: {name}","cam.notice.download":"已开始下载: {name}","cam.notice.downloadWithSidecar":"已下载: {name}(含侧车 {sidecar})","cam.notice.fileDeleted":"文件已删除: {name}","cam.notice.quickPresetApplied":"已应用快速预设: {preset}","cam.notice.needPreview":"请先启动预览","cam.notice.needManualForAutoAdjust":"请先切换为手动曝光再进行智能调整","cam.notice.autoAdjusted":"智能调整已完成并生效","cam.confirm.reset":"将重置当前设备的相机设置。确认继续?","cam.confirm.deletePreset":"确定删除预设 {name} 吗?","cam.confirm.deleteFile":"确定删除文件 {name} 吗?","sys.sensors.mag.calStatusPrefix":"校准状态:","sys.sensors.mag.calModeAuto":"自动模式(未锁定)","sys.sensors.mag.calModeRecording":"录制中(请旋转设备)","sys.sensors.mag.calModeLocked":"已锁定(使用保存方向)","sys.sensors.mag.calSamplesPrefix":"录制样本:","sys.sensors.mag.calRecordingHint":"操作提示:保持水平,缓慢旋转设备,建议累计 10+ 样本后保存。","sys.sensors.mag.calLockedHint":"已锁定方向参数:"},g=a.createContext(null),p={zh:C,en:k},h="ogscope.analysis.locale";function u(){const e=window.localStorage.getItem(h);return e==="zh"||e==="en"?e:(navigator.language||"zh").toLowerCase().startsWith("en")?"en":"zh"}function A({children:e}){const[s,t]=a.useState(u),[o,i]=a.useState(p[u()]);a.useEffect(()=>{i(p[s]),window.localStorage.setItem(h,s),document.documentElement.lang=s==="en"?"en":"zh-CN"},[s]);const r=a.useMemo(()=>(n,c)=>{let l=o[n]??n;if(c)for(const[d,b]of Object.entries(c))l=l.replace(new RegExp(`\\{${d}\\}`,"g"),String(b));return l},[o]),m=a.useMemo(()=>({locale:s,setLocale:t,t:r}),[s,r]);return f.jsx(g.Provider,{value:m,children:e})}function x(){const e=a.useContext(g);if(!e)throw new Error("useI18n must be used within I18nProvider");return e}export{A as I,R as T,P as c,x as u}; diff --git a/web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js b/web/static/analysis-lab/assets/refresh-cw-BwAka9-0.js similarity index 91% rename from web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js rename to web/static/analysis-lab/assets/refresh-cw-BwAka9-0.js index 57f12de..f2382ab 100644 --- a/web/static/analysis-lab/assets/refresh-cw-CIJtimxg.js +++ b/web/static/analysis-lab/assets/refresh-cw-BwAka9-0.js @@ -1,4 +1,4 @@ -import{c as e}from"./index-CV2LaaT8.js";/** +import{c as e}from"./index-DfDaIsmY.js";/** * @license lucide-react v0.460.0 - ISC * * This source code is licensed under the ISC license. diff --git a/web/static/analysis-lab/assets/rotate-ccw-B0-EEdqV.js b/web/static/analysis-lab/assets/rotate-ccw-B0-EEdqV.js new file mode 100644 index 0000000..507190a --- /dev/null +++ b/web/static/analysis-lab/assets/rotate-ccw-B0-EEdqV.js @@ -0,0 +1,6 @@ +import{c as t}from"./index-DfDaIsmY.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 e=t("RotateCcw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);export{e as R}; diff --git a/web/static/analysis-lab/assets/system-BMLXYbaG.js b/web/static/analysis-lab/assets/system-BqTKpy33.js similarity index 99% rename from web/static/analysis-lab/assets/system-BMLXYbaG.js rename to web/static/analysis-lab/assets/system-BqTKpy33.js index 217e731..b91d241 100644 --- a/web/static/analysis-lab/assets/system-BMLXYbaG.js +++ b/web/static/analysis-lab/assets/system-BqTKpy33.js @@ -1,4 +1,4 @@ -import{r,j as e,a as Re,R as Oe}from"./client-D1ZVDB-N.js";import{u as ye,X as Te,C as Ae,r as K,a as W,S as Fe,b as ze}from"./http-lbh6zBy9.js";import{c as U,u as V,T as Ie,I as De}from"./index-CV2LaaT8.js";import{R as ve}from"./refresh-cw-CIJtimxg.js";/** +import{r,j as e,a as Re,R as Oe}from"./client-D1ZVDB-N.js";import{u as ye,X as Te,C as Ae,r as K,a as W,S as Fe,b as ze}from"./http-BxpNn71N.js";import{c as U,u as V,T as Ie,I as De}from"./index-DfDaIsmY.js";import{R as ve}from"./refresh-cw-BwAka9-0.js";/** * @license lucide-react v0.460.0 - ISC * * This source code is licensed under the ISC license. diff --git a/web/static/analysis-lab/camera.html b/web/static/analysis-lab/camera.html index 4dafffb..a2c6498 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,11 +4,12 @@ OGScope 相机调试控制台 - + - - - + + + +
diff --git a/web/static/analysis-lab/index.html b/web/static/analysis-lab/index.html index 1aee890..1d2aba7 100644 --- a/web/static/analysis-lab/index.html +++ b/web/static/analysis-lab/index.html @@ -4,11 +4,12 @@ OGScope 星空解算控制台 - + - - - + + + +
diff --git a/web/static/analysis-lab/system.html b/web/static/analysis-lab/system.html index f12f1ca..037f9de 100644 --- a/web/static/analysis-lab/system.html +++ b/web/static/analysis-lab/system.html @@ -4,12 +4,12 @@ OGScope 系统调试控制台 - + - - - - + + + +
diff --git a/web/static/i18n/analysis.en.json b/web/static/i18n/analysis.en.json index 8237c67..07984f5 100644 --- a/web/static/i18n/analysis.en.json +++ b/web/static/i18n/analysis.en.json @@ -365,6 +365,43 @@ "cam.preview.mode": "Exposure Mode", "cam.preview.emptyTitle": "Preview is not started", "cam.preview.emptyDesc": "Start the camera preview stream with the button below.", + "cam.focus.title": "Star Focus Calibration", + "cam.focus.intro": "Measures focus from multiple stars in raw frames. Rotate the lens slowly after starting; lower HFD is sharper, and the yellow ring marks the reference star.", + "cam.focus.start": "Start Focus Calibration", + "cam.focus.starting": "Preparing camera…", + "cam.focus.finish": "Finish Calibration", + "cam.focus.resetBest": "Reset Session Best", + "cam.focus.autoSelect": "Resume Auto Selection", + "cam.focus.active": "Measuring", + "cam.focus.idle": "Not started", + "cam.focus.currentHfd.multi": "Current Multi-star HFD", + "cam.focus.currentHfd.single": "Current Locked-star HFD", + "cam.focus.bestHfd": "Session Best", + "cam.focus.lowerBetter": "lower is better", + "cam.focus.stars": "Usable / Detected Stars", + "cam.focus.fwhm": "Median FWHM", + "cam.focus.concentration": "Core Concentration", + "cam.focus.selectedStar": "Reference Star", + "cam.focus.stability": "In-frame Spread", + "cam.focus.history": "Recent Focus Trend", + "cam.focus.historyEmpty": "Collecting focus samples…", + "cam.focus.clickHint": "Click the preview to lock a star", + "cam.focus.autoTarget": "Automatically selecting a high-SNR star", + "cam.focus.manualTarget": "Locked near the clicked position", + "cam.focus.confidence.high": "High confidence", + "cam.focus.confidence.medium": "Medium confidence", + "cam.focus.confidence.low": "Low confidence; find more stars", + "cam.focus.guidance.collecting": "Keep the device still while a baseline is collected…", + "cam.focus.guidance.noStars": "No usable stars found. Check sky direction, exposure, and the lens cap.", + "cam.focus.guidance.best": "Near the session best. Fine-tune, hold for a few seconds, then lock the lens when stable.", + "cam.focus.guidance.improving": "Focus is improving. Continue with a small turn in the same direction.", + "cam.focus.guidance.worsening": "Focus is getting worse. Reverse slightly and return to the session best.", + "cam.focus.guidance.steady": "The change is small. Use a finer adjustment and watch for a lower session best.", + "cam.focus.warning.saturated": "Some bright stars were saturated and excluded. Lower exposure or gain if too few stars remain.", + "cam.focus.warning.targetNotFound": "No usable star was found near that point. Click closer to a star center or resume automatic selection.", + "cam.focus.step.start": "Point at the sky, start calibration, and wait for at least three usable stars.", + "cam.focus.step.adjust": "Rotate the lens slowly and wait for the curve to settle after each adjustment.", + "cam.focus.step.lock": "Return to the lowest HFD, confirm stability, and lock the lens.", "cam.state.streaming": "streaming", "cam.state.idle": "idle", "cam.state.rec": "REC", diff --git a/web/static/i18n/analysis.zh.json b/web/static/i18n/analysis.zh.json index d66535b..11b3ebe 100644 --- a/web/static/i18n/analysis.zh.json +++ b/web/static/i18n/analysis.zh.json @@ -366,6 +366,43 @@ "cam.preview.mode": "曝光模式", "cam.preview.emptyTitle": "预览尚未启动", "cam.preview.emptyDesc": "点击下方按钮启动相机预览流。", + "cam.focus.title": "星点焦点校准", + "cam.focus.intro": "用原始帧中的多颗星测量焦点质量。开始后缓慢旋转镜头;HFD 越小越清晰,黄色圆圈表示当前参考星。", + "cam.focus.start": "开始焦点校准", + "cam.focus.starting": "准备相机…", + "cam.focus.finish": "完成校准", + "cam.focus.resetBest": "重置本轮最佳", + "cam.focus.autoSelect": "恢复自动选星", + "cam.focus.active": "测量中", + "cam.focus.idle": "未开始", + "cam.focus.currentHfd.multi": "当前多星 HFD", + "cam.focus.currentHfd.single": "当前锁定星 HFD", + "cam.focus.bestHfd": "本轮最佳", + "cam.focus.lowerBetter": "越低越好", + "cam.focus.stars": "可用星 / 检测星", + "cam.focus.fwhm": "中位 FWHM", + "cam.focus.concentration": "核心能量集中度", + "cam.focus.selectedStar": "参考星", + "cam.focus.stability": "帧内离散度", + "cam.focus.history": "最近焦点趋势", + "cam.focus.historyEmpty": "正在收集焦点样本…", + "cam.focus.clickHint": "可点击预览锁定一颗星", + "cam.focus.autoTarget": "自动选择高信噪比星点", + "cam.focus.manualTarget": "已按点击位置锁定", + "cam.focus.confidence.high": "高置信度", + "cam.focus.confidence.medium": "中等置信度", + "cam.focus.confidence.low": "低置信度,请寻找更多星点", + "cam.focus.guidance.collecting": "保持设备稳定,正在建立基线…", + "cam.focus.guidance.noStars": "没有找到可用星点:请检查天空方向、曝光和镜头保护盖。", + "cam.focus.guidance.best": "已接近本轮最佳;微调并保持数秒,确认读数稳定后锁紧镜头。", + "cam.focus.guidance.improving": "正在变清晰,继续沿当前方向小幅旋转。", + "cam.focus.guidance.worsening": "清晰度变差,建议反向微调并回到本轮最佳。", + "cam.focus.guidance.steady": "变化很小;请用更小角度微调,观察本轮最佳是否下降。", + "cam.focus.warning.saturated": "部分亮星已饱和并被排除;若可用星过少,请降低曝光或增益。", + "cam.focus.warning.targetNotFound": "点击位置附近没有可用星点,请点击更靠近星点中心的位置,或恢复自动选星。", + "cam.focus.step.start": "对准星空并开始校准,等待至少三颗可用星。", + "cam.focus.step.adjust": "缓慢旋转镜头,每次调整后等待曲线稳定。", + "cam.focus.step.lock": "回到本轮最低 HFD,保持稳定后锁紧镜头。", "cam.state.streaming": "流已启动", "cam.state.idle": "未启动", "cam.state.rec": "录制中", From c50a10abcf5bd6b5802dbfb784cd4337e6c03ae6 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, 25 Aug 2026 12:55:36 +0800 Subject: [PATCH 06/24] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E6=98=9F?= =?UTF-8?q?=E5=9B=BE=E8=A7=A3=E7=AE=97=E5=B9=B6=E7=94=A8=E7=A7=92=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=9B=9D=E5=85=89=20/=20unify=20plate=20solving=20and?= =?UTF-8?q?=20show=20exposure=20in=20seconds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contracts/dev-rest-v1.md | 8 +++ ogscope/core/realtime/service.py | 27 ++++---- tests/unit/test_realtime_solve_pipeline.py | 30 +++++++++ web/spa/src/apps/camera/CameraConsoleApp.tsx | 52 +++++++++++----- web/spa/src/shared/utils/metaCaption.ts | 6 +- .../analysis-lab/assets/analysis-CpY6-RHN.js | 46 -------------- .../analysis-lab/assets/analysis-D9fGj8-W.js | 46 ++++++++++++++ .../analysis-lab/assets/camera-9osQb5iQ.js | 61 +++++++++++++++++++ .../analysis-lab/assets/camera-l9M7WKlh.js | 61 ------------------- .../{http-BxpNn71N.js => http-BYDXiV1S.js} | 2 +- .../analysis-lab/assets/index-CvoLNW13.js | 26 ++++++++ .../analysis-lab/assets/index-DfDaIsmY.js | 26 -------- ...-cw-BwAka9-0.js => refresh-cw-BwLohGzZ.js} | 2 +- ...ccw-B0-EEdqV.js => rotate-ccw-DCo4kEpd.js} | 2 +- ...{system-BqTKpy33.js => system-B-ZWtLCZ.js} | 2 +- web/static/analysis-lab/camera.html | 8 +-- web/static/analysis-lab/index.html | 8 +-- web/static/analysis-lab/system.html | 8 +-- web/static/i18n/analysis.en.json | 4 +- web/static/i18n/analysis.zh.json | 4 +- 20 files changed, 245 insertions(+), 184 deletions(-) create mode 100644 tests/unit/test_realtime_solve_pipeline.py delete mode 100644 web/static/analysis-lab/assets/analysis-CpY6-RHN.js create mode 100644 web/static/analysis-lab/assets/analysis-D9fGj8-W.js create mode 100644 web/static/analysis-lab/assets/camera-9osQb5iQ.js delete mode 100644 web/static/analysis-lab/assets/camera-l9M7WKlh.js rename web/static/analysis-lab/assets/{http-BxpNn71N.js => http-BYDXiV1S.js} (97%) create mode 100644 web/static/analysis-lab/assets/index-CvoLNW13.js delete mode 100644 web/static/analysis-lab/assets/index-DfDaIsmY.js rename web/static/analysis-lab/assets/{refresh-cw-BwAka9-0.js => refresh-cw-BwLohGzZ.js} (91%) rename web/static/analysis-lab/assets/{rotate-ccw-B0-EEdqV.js => rotate-ccw-DCo4kEpd.js} (88%) rename web/static/analysis-lab/assets/{system-BqTKpy33.js => system-B-ZWtLCZ.js} (99%) diff --git a/docs/contracts/dev-rest-v1.md b/docs/contracts/dev-rest-v1.md index d5e227b..a117693 100644 --- a/docs/contracts/dev-rest-v1.md +++ b/docs/contracts/dev-rest-v1.md @@ -42,6 +42,7 @@ - `aeFlickerMode` - `noiseReductionMode` - `previewEncoder` + - 开发 UI 的曝光输入与普通摘要统一显示为秒;提交到现有 API、预设和侧车时仍换算并保留 `exposure_us` / `autoExposureMaxUs` 微秒字段,避免破坏兼容性。 ### 星点焦点校准 @@ -58,6 +59,13 @@ ## 分析实验扩展 +### 解算管线边界 + +- `/api/core/v1/analysis/*` 与 `/api/dev/analysis/*` 保留不同的产品/实验接口职责,但两者的当前帧星图解算统一使用 `PlateSolver.solve_from_bgr_frame()` 的 Tetra 提星与匹配管线。 +- 开发接口可覆盖提星参数和实验分档;Core 使用服务端生产默认值,不向上层暴露实验分档。 +- `StarExtractor` 仍用于焦点校准、轻量星点计数和性能基线,不作为产品级 plate solve 的权威提星结果。 +- 历史的 Core `StarExtractor -> PlateSolver.solve()` 组合已停止使用;这是内部实现迁移,不弃用任何 `/api/core/v1/*` 或 `/api/dev/*` HTTP 路径。 + - `POST /api/dev/analysis/solve/frame` - `POST /api/dev/analysis/solve/frame_upload` - 请求可选 `enable_polar_guide`。 diff --git a/ogscope/core/realtime/service.py b/ogscope/core/realtime/service.py index 61268b1..a4eb4a7 100644 --- a/ogscope/core/realtime/service.py +++ b/ogscope/core/realtime/service.py @@ -11,7 +11,6 @@ from ogscope.algorithms.plate_solve import PlateSolver, SolveResult from ogscope.algorithms.plate_solve.sensor_context import attach_sensor_prediction -from ogscope.algorithms.star_extract import StarExtractor, StarPoint from ogscope.config import effective_solver_max_stars, get_settings from ogscope.web.camera_shared import get_camera_manager @@ -32,7 +31,7 @@ class RealtimeSolveService: def __init__(self) -> None: settings = get_settings() - self.extractor = StarExtractor(max_stars=effective_solver_max_stars(settings)) + self._max_stars = effective_solver_max_stars(settings) self.solver = PlateSolver( fov_deg=settings.solver_fov_deg, fov_max_error_deg=settings.solver_fov_max_error_deg, @@ -40,7 +39,7 @@ def __init__(self) -> None: ) self.state = RealtimeState() self._task: asyncio.Task[None] | None = None - self._previous_stars: list[StarPoint] | None = None + self._has_fullsolve = False self._hint_ra = settings.solver_hint_ra_deg self._hint_dec = settings.solver_hint_dec_deg self._fullsolve_interval = max(1, settings.solver_fullsolve_interval_frames) @@ -77,7 +76,7 @@ async def start( self._solve_timeout_ms = solve_timeout_ms self._solve_context = solve_context self.state = RealtimeState(running=True) - self._previous_stars = None + self._has_fullsolve = False self._task = asyncio.create_task(self._loop()) return {"success": True, "message": "实时解算已启动 / Realtime solver started"} @@ -134,22 +133,27 @@ async def _loop(self) -> None: continue last_frame_id = frame_id last_started_mono = time.monotonic() - stars = self.extractor.extract(frame) self.state.frame_count += 1 use_fullsolve = ( self.state.frame_count % self._fullsolve_interval == 0 - or self._previous_stars is None + or not self._has_fullsolve + or str((self.state.last_result or {}).get("status") or "") + != "MATCH_FOUND" ) if use_fullsolve: solved = await asyncio.to_thread( self._solve_frame_sync, frame, - stars, ) self._apply_solve_result(solved) self.state.fullsolve_count += 1 - self._previous_stars = stars + # ``solve_from_bgr_frame`` is the authoritative production + # image pipeline. Keep only a sentinel here; StarExtractor + # remains available for focus metrics and lightweight counts. + # ``solve_from_bgr_frame`` 是生产解算权威图像管线;这里只保留哨兵。 + # StarExtractor 仍用于焦点指标和轻量星点计数。 + self._has_fullsolve = True except Exception as exc: # noqa: BLE001 self.state.last_error = str(exc) await asyncio.sleep(0.1) @@ -157,12 +161,11 @@ async def _loop(self) -> None: def _solve_frame_sync( self, frame: Any, - stars: list[StarPoint], ) -> SolveResult: """同步解算单帧(线程池中调用)/ Sync solve for one frame.""" - return self.solver.solve( - stars=stars, - frame_shape=frame.shape, + return self.solver.solve_from_bgr_frame( + frame_bgr=frame, + max_stars=self._max_stars, hint_ra_deg=self._hint_ra, hint_dec_deg=self._hint_dec, solve_source="realtime", diff --git a/tests/unit/test_realtime_solve_pipeline.py b/tests/unit/test_realtime_solve_pipeline.py new file mode 100644 index 0000000..7a4f231 --- /dev/null +++ b/tests/unit/test_realtime_solve_pipeline.py @@ -0,0 +1,30 @@ +"""Core realtime solve pipeline tests / Core 实时解算管线测试。""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import numpy as np + +from ogscope.core.realtime.service import RealtimeSolveService + + +def test_core_realtime_uses_authoritative_bgr_pipeline() -> None: + """Core 与开发分析共用 Tetra 图像入口 / Core shares the Tetra image entry.""" + service = RealtimeSolveService() + service.solver = MagicMock() + frame = np.zeros((32, 48, 3), dtype=np.uint8) + + service._solve_frame_sync(frame) + + service.solver.solve_from_bgr_frame.assert_called_once_with( + frame_bgr=frame, + max_stars=service._max_stars, + hint_ra_deg=service._hint_ra, + hint_dec_deg=service._hint_dec, + solve_source="realtime", + fov_estimate=service._fov_estimate, + fov_max_error=service._fov_max_error, + solve_timeout_ms=service._solve_timeout_ms, + ) + service.solver.solve.assert_not_called() diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index ba9a122..695adc2 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -161,7 +161,7 @@ type FocusSample = { }; type CameraForm = { - exposure: number; + exposureSeconds: number; gain: number; digitalGain: number; autoExposure: boolean; @@ -172,7 +172,7 @@ type CameraForm = { noiseReduction: number; noiseReductionMode: string; aeFlickerMode: string; - autoExposureMaxUs: number; + autoExposureMaxSeconds: number; whiteBalanceMode: string; whiteBalanceGainR: number; whiteBalanceGainB: number; @@ -233,6 +233,27 @@ function toNum(v: unknown, fallback: number): number { return Number(v); } +function exposureUsToSeconds(value: unknown, fallbackUs: number): number { + return toNum(value, fallbackUs) / 1_000_000; +} + +function exposureSecondsToUs(value: number): number { + return Math.round(value * 1_000_000); +} + +function formatExposureSeconds(valueUs: unknown): string { + return `${exposureUsToSeconds(valueUs, 0).toFixed(4)} s`; +} + +function cameraSettingsPayload(form: CameraForm): Record { + const { exposureSeconds, autoExposureMaxSeconds, ...settings } = form; + return { + ...settings, + exposure: exposureSecondsToUs(exposureSeconds), + autoExposureMaxUs: exposureSecondsToUs(autoExposureMaxSeconds), + }; +} + function formatSize(bytes: number): string { if (!bytes) return "0 B"; const units = ["B", "KB", "MB", "GB"]; @@ -313,12 +334,13 @@ function ParamSlider({ disabled?: boolean; unit?: string; }) { + const decimals = step >= 1 ? 0 : Math.min(6, Math.max(1, Math.ceil(-Math.log10(step)))); return (
+
+ {t("cam.controls.exposure")}: + + {streamMetrics?.actual_exposure_us != null || status?.info?.actual_exposure_us != null + ? formatExposureSeconds(streamMetrics?.actual_exposure_us ?? status?.info?.actual_exposure_us ?? 0) + : "—"} + +

+ {status?.info?.auto_exposure ? t("cam.controls.auto") : t("cam.controls.manual")} +

+
{t("cam.stats.encodeMs")}: @@ -2130,7 +2141,7 @@ export function CameraConsoleApp() { )} {exposureLocked &&

{t("cam.controls.lockedByAe")}

}
- { setFormDirty(true); setForm((p) => ({ ...p, exposureSeconds: v })); }} /> + { setFormDirty(true); setForm((p) => ({ ...p, exposureSeconds: v })); }} /> { setFormDirty(true); setForm((p) => ({ ...p, gain: Number(v.toFixed(1)) })); }} /> { setFormDirty(true); setForm((p) => ({ ...p, digitalGain: Number(v.toFixed(1)) })); }} /> { setForm((p) => ({ ...p, whiteBalanceGainR: Number(v.toFixed(1)) })); setFormDirty(true); }} /> { setForm((p) => ({ ...p, whiteBalanceGainB: Number(v.toFixed(1)) })); setFormDirty(true); }} /> diff --git a/web/static/analysis-lab/assets/camera-BqtkZqLT.js b/web/static/analysis-lab/assets/camera-BqtkZqLT.js deleted file mode 100644 index a4ef4a8..0000000 --- a/web/static/analysis-lab/assets/camera-BqtkZqLT.js +++ /dev/null @@ -1,61 +0,0 @@ -import{r as o,j as e,a as fr,R as br}from"./client-D1ZVDB-N.js";import{c as P,u as gr,T as yr,I as vr}from"./index-D6H2w4fH.js";import{u as jr,S as wr,C as Sa,X as Nr,r as Sr,b as kr}from"./http-CgJePrP1.js";import{R as _r}from"./rotate-ccw-B_3LyDy-.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 ka=P("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** - * @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 bt=P("Crosshair",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]]);/** - * @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 Cr=P("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** - * @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 Fr=P("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** - * @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 Rr=P("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** - * @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 Er=P("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** - * @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 $r=P("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** - * @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 _a=P("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** - * @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 Br=P("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** - * @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 Pr=P("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** - * @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 Mr=P("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** - * @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 Tr=P("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Ir="/api/dev/debug";function H(a){return`${Ir}${a}`}async function h(a,b={}){const g=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Sr(g,b)}const Hr=["640x360","1280x720","1600x900","1920x1020"],Or=[0,90,180,270],Ve=12;function k(a,b,g){return Math.min(g,Math.max(b,a))}function F(a,b){return a==null||Number.isNaN(Number(a))?b:Number(a)}function ie(a,b){return F(a,b)/1e6}function gt(a){return Math.round(a*1e6)}function Ca(a){return`${ie(a,0).toFixed(4)} s`}function Fa(a){const{exposureSeconds:b,autoExposureMaxSeconds:g,...f}=a;return{...f,exposure:gt(b),autoExposureMaxUs:gt(g)}}function Ra(a){if(!a)return"0 B";const b=["B","KB","MB","GB"];let g=0,f=a;for(;f>=1024&&gq.hfd),f=Math.min(...g),n=Math.max(...g),ce=Math.max(.2,n-f),x=g.map((q,K)=>{const le=K/Math.max(1,g.length-1)*320,l=76-(q-f)/ce*60;return`${le.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:x,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((z=x.split(" ").at(-1))==null?void 0:z.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Ea(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const b=await a.json(),g=Number(b.max_clients??0),f=Number(b.active_clients??0);return g>0&&f>=g?"busy":"ok"}catch{return"fail"}}function G({label:a,value:b,min:g,max:f,step:n,onChange:ce,disabled:x=!1,unit:z=""}){const q=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${x?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(q),z]})]}),e.jsx("input",{type:"range",min:g,max:f,step:n,value:b,disabled:x,onChange:K=>ce(Number(K.target.value)),className:"w-full accent-primary"})]})}function Dr(){var ia,ca,la,da,ua,ma,xa,pa,ha,fa,ba,ga,ya,va,ja,wa;const{t:a,locale:b,setLocale:g}=gr(),{info:f}=jr(),[n,ce]=o.useState(null),[x,z]=o.useState(!1),[q,K]=o.useState(()=>Date.now()),[le,l]=o.useState(null),[ve,Z]=o.useState(null),[yt,Q]=o.useState(!1),[de,y]=o.useState(null),[M,je]=o.useState(!1),[Je,vt]=o.useState(!1),[jt,wt]=o.useState(!1),[Nt,St]=o.useState("8"),[kt,_t]=o.useState("8"),[Ct,Ft]=o.useState("1280x720"),[Rt,Et]=o.useState("supersample"),[ue,ee]=o.useState(!1),[$t,$a]=o.useState(!0),[Ke,Ba]=o.useState(!0),[Xe,Pa]=o.useState(!1),[Bt,Ma]=o.useState(!1),[Pt,Mt]=o.useState(!1),[Ye,Ta]=o.useState({mean:0,std:0,over:0}),[Ia,we]=o.useState(0),[i,Ze]=o.useState(null),[S,Qe]=o.useState(!1),[te,Ne]=o.useState(!1),[s,Tt]=o.useState(null),[Se,me]=o.useState([]),[ae,xe]=o.useState(null),[j,et]=o.useState(null),[It,O]=o.useState(null),[Ht,Ot]=o.useState(0),[At,Dt]=o.useState(180),[ke,Gt]=o.useState(!1),[_e,zt]=o.useState(!1),[u,N]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:2,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ce,w]=o.useState(!1),[tt,qt]=o.useState(""),[Wt,Lt]=o.useState(""),[Ut,Ha]=o.useState([]),[at,W]=o.useState(!1),[pe,Oa]=o.useState([]),[Vt,Jt]=o.useState(!1),[_,rt]=o.useState(null),[Aa,nt]=o.useState(!1),[Fe,st]=o.useState(null),[ot,Re]=o.useState(1),$=o.useRef(null),Ee=o.useRef(null),he=o.useRef(null),$e=o.useRef(null),Be=o.useRef(null),L=o.useRef(null),Pe=o.useRef(null),it=o.useRef(!1),re=o.useRef(!1),X=o.useRef(!1),Me=o.useRef(null),U=o.useRef([]),ne=o.useRef(0),ct=o.useRef(null),Kt=()=>{U.current=[],ne.current=0,Me.current=null,we(0)},R=async()=>{try{const t=await h("/api/debug/camera/status",{cache:"no-store"});ce(t),t.streaming||(z(!1),X.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},fe=()=>{Pe.current!=null&&(window.clearTimeout(Pe.current),Pe.current=null)},lt=async()=>{if(M)return!1;je(!0),l(null),Z(null),Q(!1);try{fe(),n!=null&&n.streaming||await h("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Ea();return r==="busy"?(Z(a("cam.err.streamBusy")),Q(!0),!1):r==="fail"?(Z(a("cam.err.streamProbeFailed")),Q(!1),!1):(z(!0),X.current=!0,y(a("cam.notice.previewStart")),Kt(),Me.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{je(!1)}},Xt=async()=>{if(!te){Ne(!0),O(null);try{if(!(x||await lt()))return;const r=await h(H("/camera/focus/session/start"),{method:"POST"});re.current=r.active,N(c=>({...c,autoExposure:!1,exposureSeconds:ie(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),Tt(null),me([]),xe(null),et(null),Mt(!0),Qe(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},dt=async()=>{if(!te){Ne(!0),Qe(!1),O(null);try{if(re.current){const t=await h(H("/camera/focus/session/stop"),{method:"POST"});re.current=!1,t.restored_settings&&(N(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:ie(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},Da=()=>{me([]),xe(null),O(null)},Ga=t=>{if(!S||!$.current||t.target.closest("button, label, input"))return;const r=$.current,c=t.currentTarget.getBoundingClientRect(),p=r.naturalWidth||(s==null?void 0:s.frame.width)||0,d=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(p<=0||d<=0)return;const m=Math.min(c.width/p,c.height/d),C=p*m,v=d*m,E=(c.width-C)/2,B=(c.height-v)/2,D=(t.clientX-c.left-E)/C,se=(t.clientY-c.top-B)/v;D<0||D>1||se<0||se>1||(et({x:D,y:se}),me([]),xe(null),O(null))},za=()=>{et(null),me([]),xe(null),O(null)},qa=async()=>{if(!M){je(!0),l(null);try{fe(),z(!1),X.current=!1,Z(null),Q(!1),Kt(),$.current&&($.current.onload=null,$.current.onerror=null,$.current.src="",$.current.removeAttribute("src")),K(Date.now()),y(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{je(!1)}}},Wa=async()=>{if(!(!x&&!(n!=null&&n.streaming))){wt(!0),l(null);try{const t=await h("/api/debug/camera/capture",{method:"POST"});y(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{wt(!1)}}},La=async()=>{if(!Je){vt(!0),l(null);try{if(n!=null&&n.recording)await h("/api/debug/camera/record/stop",{method:"POST"}),y(a("cam.notice.recordStop"));else{const t=await h("/api/debug/camera/record/start",{method:"POST"});y(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{vt(!1)}}},Ua=async()=>{l(null);try{const t=k(parseInt(Nt,10)||5,1,60);await h(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=k(parseInt(kt,10)||8,1,30);await h(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,p]=Ct.split("x").map(d=>parseInt(d,10));c&&p&&await h(`/api/debug/camera/size?width=${c}&height=${p}`,{method:"POST"}),await h(`/api/debug/camera/sampling?mode=${encodeURIComponent(Rt)}`,{method:"POST"}),y(a("cam.notice.runtimeApplied")),ee(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Va=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Fa(u))}),y(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ja=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Fa(u))}),y(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ka=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;N({exposureSeconds:k(ie(t.exposure_us,5e3),1e-4,.12),gain:k(F(t.analogue_gain,1),1,24),digitalGain:k(F(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:k(F(t.contrast,1),0,2),brightness:k(F(t.brightness,0),-1,1),saturation:k(F(t.saturation,1),0,2),sharpness:k(F(t.sharpness,1),0,2),noiseReduction:k(Math.round(F(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:k(ie(t.auto_exposure_max_us,2e6),.01,10),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:k(F(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:k(F(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(St(String(Math.round(F(t.fps,8)))),Ft(`${Math.round(F(t.width,1280))}x${Math.round(F(t.height,720))}`),Et(String(t.sampling_mode??"supersample")),ee(!1)),Dt(k(Math.round(F(t.rotation,180)),0,270)),Gt(!!t.flip_horizontal),zt(!!t.flip_vertical),w(!1)},Yt=async(t,r)=>{l(null);try{await h("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),Gt(t),zt(r),y(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},Xa=async t=>{l(null);try{await h(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Dt(t),y(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},Zt=async t=>{l(null);try{await h(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),y(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},Ya=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await h("/api/debug/camera/reset",{method:"POST"}),y(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},Za=async()=>{l(null);try{await h("/api/debug/camera/backup-settings",{method:"POST"}),y(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},Qa=async()=>{l(null);try{await h("/api/debug/camera/restore-settings",{method:"POST"}),y(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Te=async()=>{W(!0);try{const t=await h("/api/debug/camera/presets",{cache:"no-store"});Ha(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{W(!1)}},er=async()=>{const t=tt.trim();if(t){W(!0),l(null);try{await h("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Wt.trim(),exposure_us:gt(u.exposureSeconds),analogue_gain:u.gain,digital_gain:u.digitalGain,auto_exposure:u.autoExposure,contrast:u.contrast,brightness:u.brightness,saturation:u.saturation,sharpness:u.sharpness,noise_reduction:u.noiseReduction,white_balance_mode:u.whiteBalanceMode,white_balance_gain_r:u.whiteBalanceGainR,white_balance_gain_b:u.whiteBalanceGainB,rotation:At,flip_horizontal:ke,flip_vertical:_e,color_mode:u.colorMode})}),y(a("cam.notice.presetSaved",{name:t})),qt(""),Lt(""),await Te()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},tr=async t=>{W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),y(a("cam.notice.presetApplied",{name:t})),await R(),await Te()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}},ar=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),y(a("cam.notice.presetDeleted",{name:t})),await Te()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},ut=async()=>{Jt(!0);try{const t=await h("/api/debug/files",{cache:"no-store"});Oa(t.files??[]),Re(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Jt(!1)}},mt=()=>{st(null),rt(null),nt(!1)},rr=async t=>{if(Fe===t){mt();return}st(t),rt(null),nt(!0),l(null);try{const r=await h(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});rt(r)}catch(r){l(r instanceof Error?r.message:String(r)),st(null)}finally{nt(!1)}},nr=t=>{const r=(p,d)=>{const m=document.createElement("a");m.href=d,m.download=p,document.body.appendChild(m),m.click(),document.body.removeChild(m)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const d=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(d)}`)).ok)return;r(d,`${H("/files")}/${encodeURIComponent(d)}`),y(a("cam.notice.downloadWithSidecar",{name:t,sidecar:d}))}catch{y(a("cam.notice.download",{name:t}))}})();return}y(a("cam.notice.download",{name:t}))},sr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await h(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),y(a("cam.notice.fileDeleted",{name:t})),(Fe===t||(_==null?void 0:_.filename)===t)&&mt(),await ut()}catch(r){l(r instanceof Error?r.message:String(r))}}},or=()=>{if(!$t||!$.current||!Ee.current)return;const t=$.current;if(!t.naturalWidth||!t.naturalHeight||(he.current||(he.current=document.createElement("canvas"),$e.current=he.current.getContext("2d",{willReadFrequently:!0})),Be.current||(Be.current=Ee.current.getContext("2d")),!$e.current||!Be.current))return;const c=Math.min(1,320/t.naturalWidth),p=Math.max(1,Math.round(t.naturalWidth*c)),d=Math.max(1,Math.round(t.naturalHeight*c));he.current.width=p,he.current.height=d,$e.current.drawImage(t,0,0,p,d);const m=$e.current.getImageData(0,0,p,d).data,C=new Array(256).fill(0),v=new Array(256).fill(0),E=new Array(256).fill(0),B=new Array(256).fill(0);let D=0,se=0,Na=0;const oe=p*d;for(let I=0;I=250&&(Na+=1)}const ft=oe?D/oe:0,pr=oe?se/oe-ft*ft:0;Ta({mean:ft,std:Math.sqrt(Math.max(0,pr)),over:oe?Na/oe*100:0});const qe=Ee.current,T=Be.current,We=window.devicePixelRatio||1,be=Math.max(1,qe.clientWidth||240),ge=Math.max(1,qe.clientHeight||120);qe.width=Math.floor(be*We),qe.height=Math.floor(ge*We),T.setTransform(We,0,0,We,0,0),T.clearRect(0,0,be,ge);const hr=Math.max(1,...Ke?[Math.max(...C),Math.max(...v),Math.max(...E)]:[0],...Xe?[Math.max(...B)]:[0]),Le=(I,Ue)=>{T.beginPath(),T.strokeStyle=Ue,T.lineWidth=1.2;for(let V=0;V<256;V+=1){const ye=V/255*be,J=ge-I[V]/hr*ge;V===0?T.moveTo(ye,J):T.lineTo(ye,J)}T.stroke()};if(Ke&&(Le(C,"rgba(255,80,80,0.85)"),Le(v,"rgba(80,255,80,0.85)"),Le(E,"rgba(80,160,255,0.85)")),Xe&&Le(B,"rgba(255,255,255,0.95)"),Bt){const I=.9803921568627451*be;T.fillStyle="rgba(255,100,100,0.12)",T.fillRect(I,0,be-I,ge)}};o.useEffect(()=>{R(),Te(),ut();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ce||Ka(n.info,{syncRuntime:!ue})},[n==null?void 0:n.info,Ce,ue]),o.useEffect(()=>{if(!(n!=null&&n.recording)){L.current&&(window.clearInterval(L.current),L.current=null),Ot(0);return}if(L.current)return;const t=Date.now();return L.current=window.setInterval(()=>{Ot(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{L.current&&(window.clearInterval(L.current),L.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{X.current=x,x||(fe(),Qe(!1),re.current&&dt())},[x]),o.useEffect(()=>()=>{re.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),re.current=!1)},[]),o.useEffect(()=>{if(!S||!x)return;let t=!1;const r=async()=>{var p,d;if(!(it.current||document.hidden)){it.current=!0;try{const m=new URLSearchParams;j&&(m.set("target_x",j.x.toFixed(6)),m.set("target_y",j.y.toFixed(6)));const C=m.size>0?`?${m.toString()}`:"",v=await h(`${H("/camera/focus/metrics")}${C}`,{cache:"no-store"});if(t)return;Tt(v),O(null);const E=j?(p=v.selected_star)==null?void 0:p.hfd_px:(d=v.aggregate)==null?void 0:d.median_hfd_px;typeof E=="number"&&Number.isFinite(E)&&(me(B=>{var D;return((D=B.at(-1))==null?void 0:D.frameId)===v.frame_id?B:[...B,{frameId:v.frame_id,hfd:E}].slice(-60)}),xe(B=>B==null?E:Math.min(B,E)))}catch(m){t||O(m instanceof Error?m.message:String(m))}finally{it.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[S,x,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{x&&(U.current=[],ne.current=0)},[q,x]),o.useEffect(()=>{if(!x){U.current=[],ne.current=0,we(0);return}if(!ct.current){const d=document.createElement("canvas");d.width=32,d.height=32,ct.current=d}const r=ct.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const p=()=>{const d=$.current;if(d!=null&&d.complete&&d.naturalWidth>0)try{r.drawImage(d,0,0,32,32);const m=r.getImageData(0,0,32,32).data;let C=2166136261;for(let v=0;vwindow.cancelAnimationFrame(c)},[x]),o.useEffect(()=>{if(!x){we(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,p=U.current;for(;p.length&&p[0]window.clearInterval(t)},[x]),o.useEffect(()=>{if(!x){Ze(null);return}let t=!1;const r=async()=>{try{const p=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!p.ok||t)return;const d=await p.json();if(!t){Ze(d);const m=Number(d.preview_target_fps??d.target_preview_fps??0);Number.isFinite(m)&&m>0&&!ue&&_t(String(Math.round(m)))}}catch{t||Ze(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[x,ue]),o.useEffect(()=>{if(!de)return;const t=window.setTimeout(()=>y(null),3200);return()=>window.clearTimeout(t)},[de]),o.useEffect(()=>()=>fe(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(pe.length/Ve));ot>t&&Re(t)},[pe.length,ot]);const ir=x?`${H("/camera/stream")}?t=${q}`:"",Ie=u.autoExposure,xt=u.whiteBalanceMode==="manual",Y=((ia=n==null?void 0:n.info)==null?void 0:ia.capabilities)??{},cr=Array.isArray(Y.awb_modes)&&Y.awb_modes.length>0?Y.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],lr=Array.isArray(Y.noise_reduction_modes)&&Y.noise_reduction_modes.length>0?Y.noise_reduction_modes:["off","fast","high_quality"],Qt=Y.manual_digital_gain!==!1,ea=!!((ca=n==null?void 0:n.info)!=null&&ca.night_mode),ta=x,He=!M&&!x&&!(n!=null&&n.recording),aa=!M&&x,dr=!M&&!jt&&ta,ur=!M&&!Je&&ta,A=j?s==null?void 0:s.selected_star:null,Oe=j?(A==null?void 0:A.hfd_px)??null:((la=s==null?void 0:s.aggregate)==null?void 0:la.median_hfd_px)??null,ra=j?(A==null?void 0:A.fwhm_px)??null:((da=s==null?void 0:s.aggregate)==null?void 0:da.median_fwhm_px)??null,na=j?(A==null?void 0:A.concentration)??null:((ua=s==null?void 0:s.aggregate)==null?void 0:ua.median_concentration)??null,Ae=Oe!=null&&ae!=null&&ae>0?(Oe-ae)/ae*100:null,sa=Se.slice(-3).map(t=>t.hfd),pt=Se.slice(-6,-3).map(t=>t.hfd),ht=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let De="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")De="cam.focus.guidance.noStars";else if(Se.length>=5&&Ae!=null&&Ae<=3)De="cam.focus.guidance.best";else if(sa.length===3&&pt.length===3){const t=ht(sa)-ht(pt),r=Math.max(.04,ht(pt)*.02);De=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const mr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Ge=Math.max(1,Math.ceil(pe.length/Ve)),ze=Math.min(ot,Ge),oa=(ze-1)*Ve,xr=pe.slice(oa,oa+Ve);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Br,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Or.map(t=>e.jsxs("button",{type:"button",onClick:()=>void Xa(t),className:`rounded border px-2 py-1 ${At===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void Yt(!ke,_e),className:`rounded border px-2 py-1 ${ke?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void Yt(ke,!_e),className:`rounded border px-2 py-1 ${_e?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void Za(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(wr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void Qa(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void Ya(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void Zt(!0),className:`rounded border px-2 py-1 ${ea?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx($r,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void Zt(!1),className:`rounded border px-2 py-1 ${ea?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:tt,onChange:t=>qt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Wt,onChange:t=>Lt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:at||!tt.trim(),onClick:()=>void er(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Ut.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Ut.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",Ca(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:at,onClick:()=>void tr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:at,onClick:()=>void ar(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.cpu_usage)==null?"—":`${Number(f.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.memory_usage)==null?"—":`${Number(f.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.temperature)==null?"—":`${Number(f.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:te||M||!!(n!=null&&n.recording),onClick:()=>S?void dt():void Xt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${S?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(bt,{className:"mr-1 inline h-3.5 w-3.5"}),a(te?"cam.focus.starting":S?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(x?"cam.state.streaming":"cam.state.idle")]})]})]}),ve&&!x&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ve}),yt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${S?"cursor-crosshair":""}`,onClick:Ga,children:[x&&ve&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ve}),yt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),x?e.jsx("img",{ref:$,alt:"camera-preview",className:"h-full w-full object-contain",src:ir,onLoad:()=>{Z(null),Q(!1),or()},onError:()=>{var r;if(fe(),!X.current)return;const t=(r=$.current)==null?void 0:r.src;(async()=>{if(!X.current||!t)return;if(await Ea()==="busy"){Z(a("cam.err.streamBusy")),Q(!0);return}Pe.current=window.setTimeout(()=>{X.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Sa,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(_a,{className:"mr-1 inline h-3.5 w-3.5"}),a(M?"cam.btn.starting":"cam.btn.start")]})]}),x&&S&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(ka,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Ht/60).toString().padStart(2,"0")}:${(Ht%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Mt(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(Pt?"cam.hist.expand":"cam.hist.collapse")})}),!Pt&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:$t,onChange:t=>$a(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ke,onChange:t=>Ba(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Xe,onChange:t=>Pa(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Bt,onChange:t=>Ma(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Ee,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:Ye.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:Ye.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[Ye.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${S?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(bt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${S?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(S?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),S?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[S?e.jsx("button",{type:"button",onClick:()=>void dt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:te||M||!!(n!=null&&n.recording),onClick:()=>void Xt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(bt,{className:"mr-1 inline h-4 w-4"}),a(te?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!S,onClick:Da,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(_r,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!S||j==null,onClick:za,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),S?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:Oe==null?"—":Oe.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(De)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:ae==null?"—":`${ae.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ae==null?"—":`+${Math.max(0,Ae).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(mr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ra==null?"—":`${ra.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:na==null?"—":`${(na*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Ar,{samples:Se,emptyLabel:a("cam.focus.historyEmpty")}),It?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:It}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Ia).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((ma=n==null?void 0:n.info)==null?void 0:ma.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:ie(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Me.current!=null?`${Math.max(0,Math.round((performance.now()-Me.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((xa=n==null?void 0:n.info)==null?void 0:xa.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((pa=n==null?void 0:n.info)==null?void 0:pa.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((ha=n==null?void 0:n.info)==null?void 0:ha.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(fa=n==null?void 0:n.info)!=null&&fa.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ba=n==null?void 0:n.info)==null?void 0:ba.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((ga=n==null?void 0:n.info)==null?void 0:ga.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((ya=n==null?void 0:n.info)==null?void 0:ya.width)??"—"}x${((va=n==null?void 0:n.info)==null?void 0:va.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(ja=n==null?void 0:n.info)!=null&&ja.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(_a,{className:"inline h-4 w-4"})," ",a(M?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!aa,onClick:()=>void qa(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${aa?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Mr,{className:"inline h-4 w-4"})," ",a(M?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!dr,onClick:()=>void Wa(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Sa,{className:"inline h-4 w-4"})," ",a(jt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!ur,onClick:()=>void La(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(ka,{className:"inline h-4 w-4"})," ",Je?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Vt,onClick:()=>void ut(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Aa&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),_&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Fr,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:_.filename})]}),e.jsx("button",{type:"button",onClick:()=>mt(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(Nr,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ra(_.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:_.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(_.modified).toLocaleString()})]}),_.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:Ca(_.exposure_us)})]}),_.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:_.analogue_gain})]}),_.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:_.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[pe.length===0&&!Vt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),xr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Fe===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ra(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>nr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Cr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void rr(t.name),className:`rounded border px-2 py-1 ${Fe===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Er,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void sr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(yr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ze<=1,onClick:()=>Re(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:ze,total:Ge})}),e.jsx("button",{type:"button",disabled:ze>=Ge,onClick:()=>Re(t=>Math.min(Ge,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:Nt,onChange:t=>{St(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:kt,onChange:t=>{_t(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Rt,onChange:t=>{Et(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Hr.map(t=>e.jsx("button",{type:"button",onClick:()=>{Ft(t),ee(!0)},className:`rounded border px-2 py-1 ${Ct===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!ue,onClick:()=>void Ua(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Pr,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Ie&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(G,{label:a("cam.controls.exposure"),value:u.exposureSeconds,min:1e-4,max:.12,step:1e-4,unit:" s",disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,exposureSeconds:t}))}}),e.jsx(G,{label:a("cam.controls.gain"),value:u.gain,min:1,max:24,step:.1,disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.digitalGain"),value:u.digitalGain,min:1,max:8,step:.1,disabled:Ie||!Qt,onChange:t=>{w(!0),N(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:u.noiseReductionMode,onChange:t=>{w(!0),N(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:lr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(G,{label:a("cam.controls.contrast"),value:u.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.brightness"),value:u.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),N(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.saturation"),value:u.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.sharpness"),value:u.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),!Qt&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((wa=n==null?void 0:n.info)==null?void 0:wa.actual_digital_gain)??u.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ce,onClick:()=>void Va(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:u.autoExposure?"auto":"manual",onChange:t=>{N(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:u.colorMode,onChange:t=>{N(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:u.whiteBalanceMode,onChange:t=>{N(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:cr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:u.aeFlickerMode,onChange:t=>{N(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:10,step:.01,value:u.autoExposureMaxSeconds,onChange:t=>{N(r=>({...r,autoExposureMaxSeconds:k(Number(t.target.value)||2,.01,10)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(G,{label:"R Gain",value:u.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(G,{label:"B Gain",value:u.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!xt&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void Ja(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(le||de)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[le&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:le}),de&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:de})]})]})}fr.createRoot(document.getElementById("root")).render(e.jsx(br.StrictMode,{children:e.jsx(vr,{children:e.jsx(kr,{children:e.jsx(Dr,{})})})})); diff --git a/web/static/analysis-lab/assets/camera-DU1oMy5v.js b/web/static/analysis-lab/assets/camera-DU1oMy5v.js new file mode 100644 index 0000000..29438ca --- /dev/null +++ b/web/static/analysis-lab/assets/camera-DU1oMy5v.js @@ -0,0 +1,61 @@ +import{r as o,j as e,a as vr,R as yr}from"./client-D1ZVDB-N.js";import{c as P,u as jr,T as wr,I as Nr}from"./index-D6H2w4fH.js";import{u as Sr,S as kr,C as Fa,X as _r,r as Cr,b as Fr}from"./http-CgJePrP1.js";import{R as Rr}from"./rotate-ccw-B_3LyDy-.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 Ra=P("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** + * @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 bt=P("Crosshair",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]]);/** + * @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 Er=P("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @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 $r=P("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** + * @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 Br=P("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** + * @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 Pr=P("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** + * @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 Tr=P("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @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 Ea=P("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @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 Mr=P("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** + * @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 Ir=P("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** + * @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 Hr=P("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** + * @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 Or=P("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Ar="/api/dev/debug";function H(a){return`${Ar}${a}`}async function h(a,b={}){const g=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Cr(g,b)}const Dr=["640x360","1280x720","1600x900","1920x1020"],Gr=[0,90,180,270],Ve=12;function k(a,b,g){return Math.min(g,Math.max(b,a))}function F(a,b){return a==null||Number.isNaN(Number(a))?b:Number(a)}function ie(a,b){return F(a,b)/1e6}function vt(a){return Math.round(a*1e6)}function gt(a){return`${ie(a,0).toFixed(4)} s`}function $a(a){const{exposureSeconds:b,autoExposureMaxSeconds:g,...f}=a;return{...f,exposure:vt(b),autoExposureMaxUs:vt(g)}}function Ba(a){if(!a)return"0 B";const b=["B","KB","MB","GB"];let g=0,f=a;for(;f>=1024&&gq.hfd),f=Math.min(...g),n=Math.max(...g),ce=Math.max(.2,n-f),x=g.map((q,K)=>{const le=K/Math.max(1,g.length-1)*320,l=76-(q-f)/ce*60;return`${le.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:x,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((z=x.split(" ").at(-1))==null?void 0:z.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Pa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const b=await a.json(),g=Number(b.max_clients??0),f=Number(b.active_clients??0);return g>0&&f>=g?"busy":"ok"}catch{return"fail"}}function G({label:a,value:b,min:g,max:f,step:n,onChange:ce,disabled:x=!1,unit:z=""}){const q=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${x?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(q),z]})]}),e.jsx("input",{type:"range",min:g,max:f,step:n,value:b,disabled:x,onChange:K=>ce(Number(K.target.value)),className:"w-full accent-primary"})]})}function qr(){var ca,la,da,ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a;const{t:a,locale:b,setLocale:g}=jr(),{info:f}=Sr(),[n,ce]=o.useState(null),[x,z]=o.useState(!1),[q,K]=o.useState(()=>Date.now()),[le,l]=o.useState(null),[ye,Z]=o.useState(null),[yt,Q]=o.useState(!1),[de,v]=o.useState(null),[T,je]=o.useState(!1),[Je,jt]=o.useState(!1),[wt,Nt]=o.useState(!1),[St,kt]=o.useState("8"),[_t,Ct]=o.useState("8"),[Ft,Rt]=o.useState("1280x720"),[Et,$t]=o.useState("supersample"),[ue,ee]=o.useState(!1),[Bt,Ta]=o.useState(!0),[Ke,Ma]=o.useState(!0),[Xe,Ia]=o.useState(!1),[Pt,Ha]=o.useState(!1),[Tt,Mt]=o.useState(!1),[Ye,Oa]=o.useState({mean:0,std:0,over:0}),[Aa,we]=o.useState(0),[i,Ze]=o.useState(null),[S,Qe]=o.useState(!1),[te,Ne]=o.useState(!1),[s,It]=o.useState(null),[Se,me]=o.useState([]),[ae,xe]=o.useState(null),[j,et]=o.useState(null),[Ht,O]=o.useState(null),[Ot,At]=o.useState(0),[Dt,Gt]=o.useState(180),[ke,zt]=o.useState(!1),[_e,qt]=o.useState(!1),[u,N]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.5,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ce,w]=o.useState(!1),[tt,Wt]=o.useState(""),[Lt,Ut]=o.useState(""),[Vt,Da]=o.useState([]),[at,W]=o.useState(!1),[pe,Ga]=o.useState([]),[Jt,Kt]=o.useState(!1),[_,rt]=o.useState(null),[za,nt]=o.useState(!1),[Fe,st]=o.useState(null),[ot,Re]=o.useState(1),$=o.useRef(null),Ee=o.useRef(null),he=o.useRef(null),$e=o.useRef(null),Be=o.useRef(null),L=o.useRef(null),Pe=o.useRef(null),it=o.useRef(!1),re=o.useRef(!1),X=o.useRef(!1),Te=o.useRef(null),U=o.useRef([]),ne=o.useRef(0),ct=o.useRef(null),Xt=()=>{U.current=[],ne.current=0,Te.current=null,we(0)},R=async()=>{try{const t=await h("/api/debug/camera/status",{cache:"no-store"});ce(t),t.streaming||(z(!1),X.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},fe=()=>{Pe.current!=null&&(window.clearTimeout(Pe.current),Pe.current=null)},lt=async()=>{if(T)return!1;je(!0),l(null),Z(null),Q(!1);try{fe(),n!=null&&n.streaming||await h("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Pa();return r==="busy"?(Z(a("cam.err.streamBusy")),Q(!0),!1):r==="fail"?(Z(a("cam.err.streamProbeFailed")),Q(!1),!1):(z(!0),X.current=!0,v(a("cam.notice.previewStart")),Xt(),Te.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{je(!1)}},Yt=async()=>{if(!te){Ne(!0),O(null);try{if(!(x||await lt()))return;const r=await h(H("/camera/focus/session/start"),{method:"POST"});re.current=r.active,N(c=>({...c,autoExposure:!1,exposureSeconds:ie(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),It(null),me([]),xe(null),et(null),Mt(!0),Qe(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},dt=async()=>{if(!te){Ne(!0),Qe(!1),O(null);try{if(re.current){const t=await h(H("/camera/focus/session/stop"),{method:"POST"});re.current=!1,t.restored_settings&&(N(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:ie(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},qa=()=>{me([]),xe(null),O(null)},Wa=t=>{if(!S||!$.current||t.target.closest("button, label, input"))return;const r=$.current,c=t.currentTarget.getBoundingClientRect(),p=r.naturalWidth||(s==null?void 0:s.frame.width)||0,d=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(p<=0||d<=0)return;const m=Math.min(c.width/p,c.height/d),C=p*m,y=d*m,E=(c.width-C)/2,B=(c.height-y)/2,D=(t.clientX-c.left-E)/C,se=(t.clientY-c.top-B)/y;D<0||D>1||se<0||se>1||(et({x:D,y:se}),me([]),xe(null),O(null))},La=()=>{et(null),me([]),xe(null),O(null)},Ua=async()=>{if(!T){je(!0),l(null);try{fe(),z(!1),X.current=!1,Z(null),Q(!1),Xt(),$.current&&($.current.onload=null,$.current.onerror=null,$.current.src="",$.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{je(!1)}}},Va=async()=>{if(!(!x&&!(n!=null&&n.streaming))){Nt(!0),l(null);try{const t=await h("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Nt(!1)}}},Ja=async()=>{if(!Je){jt(!0),l(null);try{if(n!=null&&n.recording)await h("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await h("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{jt(!1)}}},Ka=async()=>{l(null);try{const t=k(parseInt(St,10)||5,1,60);await h(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=k(parseInt(_t,10)||8,1,30);await h(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,p]=Ft.split("x").map(d=>parseInt(d,10));c&&p&&await h(`/api/debug/camera/size?width=${c}&height=${p}`,{method:"POST"}),await h(`/api/debug/camera/sampling?mode=${encodeURIComponent(Et)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),ee(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Xa=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ya=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Za=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;N({exposureSeconds:k(ie(t.exposure_us,5e3),1e-4,.5),gain:k(F(t.analogue_gain,1),1,24),digitalGain:k(F(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:k(F(t.contrast,1),0,2),brightness:k(F(t.brightness,0),-1,1),saturation:k(F(t.saturation,1),0,2),sharpness:k(F(t.sharpness,1),0,2),noiseReduction:k(Math.round(F(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:k(ie(t.auto_exposure_max_us,5e5),.01,.5),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:k(F(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:k(F(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(kt(String(Math.round(F(t.fps,8)))),Rt(`${Math.round(F(t.width,1280))}x${Math.round(F(t.height,720))}`),$t(String(t.sampling_mode??"supersample")),ee(!1)),Gt(k(Math.round(F(t.rotation,180)),0,270)),zt(!!t.flip_horizontal),qt(!!t.flip_vertical),w(!1)},Zt=async(t,r)=>{l(null);try{await h("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),zt(t),qt(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},Qa=async t=>{l(null);try{await h(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Gt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},Qt=async t=>{l(null);try{await h(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},er=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await h("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},tr=async()=>{l(null);try{await h("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},ar=async()=>{l(null);try{await h("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Me=async()=>{W(!0);try{const t=await h("/api/debug/camera/presets",{cache:"no-store"});Da(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{W(!1)}},rr=async()=>{const t=tt.trim();if(t){W(!0),l(null);try{await h("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Lt.trim(),exposure_us:vt(u.exposureSeconds),analogue_gain:u.gain,digital_gain:u.digitalGain,auto_exposure:u.autoExposure,contrast:u.contrast,brightness:u.brightness,saturation:u.saturation,sharpness:u.sharpness,noise_reduction:u.noiseReduction,white_balance_mode:u.whiteBalanceMode,white_balance_gain_r:u.whiteBalanceGainR,white_balance_gain_b:u.whiteBalanceGainB,rotation:Dt,flip_horizontal:ke,flip_vertical:_e,color_mode:u.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Ut(""),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},nr=async t=>{W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}},sr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},ut=async()=>{Kt(!0);try{const t=await h("/api/debug/files",{cache:"no-store"});Ga(t.files??[]),Re(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Kt(!1)}},mt=()=>{st(null),rt(null),nt(!1)},or=async t=>{if(Fe===t){mt();return}st(t),rt(null),nt(!0),l(null);try{const r=await h(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});rt(r)}catch(r){l(r instanceof Error?r.message:String(r)),st(null)}finally{nt(!1)}},ir=t=>{const r=(p,d)=>{const m=document.createElement("a");m.href=d,m.download=p,document.body.appendChild(m),m.click(),document.body.removeChild(m)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const d=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(d)}`)).ok)return;r(d,`${H("/files")}/${encodeURIComponent(d)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:d}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},cr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await h(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Fe===t||(_==null?void 0:_.filename)===t)&&mt(),await ut()}catch(r){l(r instanceof Error?r.message:String(r))}}},lr=()=>{if(!Bt||!$.current||!Ee.current)return;const t=$.current;if(!t.naturalWidth||!t.naturalHeight||(he.current||(he.current=document.createElement("canvas"),$e.current=he.current.getContext("2d",{willReadFrequently:!0})),Be.current||(Be.current=Ee.current.getContext("2d")),!$e.current||!Be.current))return;const c=Math.min(1,320/t.naturalWidth),p=Math.max(1,Math.round(t.naturalWidth*c)),d=Math.max(1,Math.round(t.naturalHeight*c));he.current.width=p,he.current.height=d,$e.current.drawImage(t,0,0,p,d);const m=$e.current.getImageData(0,0,p,d).data,C=new Array(256).fill(0),y=new Array(256).fill(0),E=new Array(256).fill(0),B=new Array(256).fill(0);let D=0,se=0,Ca=0;const oe=p*d;for(let I=0;I=250&&(Ca+=1)}const ft=oe?D/oe:0,br=oe?se/oe-ft*ft:0;Oa({mean:ft,std:Math.sqrt(Math.max(0,br)),over:oe?Ca/oe*100:0});const qe=Ee.current,M=Be.current,We=window.devicePixelRatio||1,be=Math.max(1,qe.clientWidth||240),ge=Math.max(1,qe.clientHeight||120);qe.width=Math.floor(be*We),qe.height=Math.floor(ge*We),M.setTransform(We,0,0,We,0,0),M.clearRect(0,0,be,ge);const gr=Math.max(1,...Ke?[Math.max(...C),Math.max(...y),Math.max(...E)]:[0],...Xe?[Math.max(...B)]:[0]),Le=(I,Ue)=>{M.beginPath(),M.strokeStyle=Ue,M.lineWidth=1.2;for(let V=0;V<256;V+=1){const ve=V/255*be,J=ge-I[V]/gr*ge;V===0?M.moveTo(ve,J):M.lineTo(ve,J)}M.stroke()};if(Ke&&(Le(C,"rgba(255,80,80,0.85)"),Le(y,"rgba(80,255,80,0.85)"),Le(E,"rgba(80,160,255,0.85)")),Xe&&Le(B,"rgba(255,255,255,0.95)"),Pt){const I=.9803921568627451*be;M.fillStyle="rgba(255,100,100,0.12)",M.fillRect(I,0,be-I,ge)}};o.useEffect(()=>{R(),Me(),ut();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ce||Za(n.info,{syncRuntime:!ue})},[n==null?void 0:n.info,Ce,ue]),o.useEffect(()=>{if(!(n!=null&&n.recording)){L.current&&(window.clearInterval(L.current),L.current=null),At(0);return}if(L.current)return;const t=Date.now();return L.current=window.setInterval(()=>{At(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{L.current&&(window.clearInterval(L.current),L.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{X.current=x,x||(fe(),Qe(!1),re.current&&dt())},[x]),o.useEffect(()=>()=>{re.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),re.current=!1)},[]),o.useEffect(()=>{if(!S||!x)return;let t=!1;const r=async()=>{var p,d;if(!(it.current||document.hidden)){it.current=!0;try{const m=new URLSearchParams;j&&(m.set("target_x",j.x.toFixed(6)),m.set("target_y",j.y.toFixed(6)));const C=m.size>0?`?${m.toString()}`:"",y=await h(`${H("/camera/focus/metrics")}${C}`,{cache:"no-store"});if(t)return;It(y),O(null);const E=j?(p=y.selected_star)==null?void 0:p.hfd_px:(d=y.aggregate)==null?void 0:d.median_hfd_px;typeof E=="number"&&Number.isFinite(E)&&(me(B=>{var D;return((D=B.at(-1))==null?void 0:D.frameId)===y.frame_id?B:[...B,{frameId:y.frame_id,hfd:E}].slice(-60)}),xe(B=>B==null?E:Math.min(B,E)))}catch(m){t||O(m instanceof Error?m.message:String(m))}finally{it.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[S,x,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{x&&(U.current=[],ne.current=0)},[q,x]),o.useEffect(()=>{if(!x){U.current=[],ne.current=0,we(0);return}if(!ct.current){const d=document.createElement("canvas");d.width=32,d.height=32,ct.current=d}const r=ct.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const p=()=>{const d=$.current;if(d!=null&&d.complete&&d.naturalWidth>0)try{r.drawImage(d,0,0,32,32);const m=r.getImageData(0,0,32,32).data;let C=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[x]),o.useEffect(()=>{if(!x){we(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,p=U.current;for(;p.length&&p[0]window.clearInterval(t)},[x]),o.useEffect(()=>{if(!x){Ze(null);return}let t=!1;const r=async()=>{try{const p=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!p.ok||t)return;const d=await p.json();if(!t){Ze(d);const m=Number(d.preview_target_fps??d.target_preview_fps??0);Number.isFinite(m)&&m>0&&!ue&&Ct(String(Math.round(m)))}}catch{t||Ze(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[x,ue]),o.useEffect(()=>{if(!de)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[de]),o.useEffect(()=>()=>fe(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(pe.length/Ve));ot>t&&Re(t)},[pe.length,ot]);const dr=x?`${H("/camera/stream")}?t=${q}`:"",Ie=u.autoExposure,xt=u.whiteBalanceMode==="manual",Y=((ca=n==null?void 0:n.info)==null?void 0:ca.capabilities)??{},ur=Array.isArray(Y.awb_modes)&&Y.awb_modes.length>0?Y.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],mr=Array.isArray(Y.noise_reduction_modes)&&Y.noise_reduction_modes.length>0?Y.noise_reduction_modes:["off","fast","high_quality"],ea=Y.manual_digital_gain!==!1,ta=!!((la=n==null?void 0:n.info)!=null&&la.night_mode),aa=x,He=!T&&!x&&!(n!=null&&n.recording),ra=!T&&x,xr=!T&&!wt&&aa,pr=!T&&!Je&&aa,A=j?s==null?void 0:s.selected_star:null,Oe=j?(A==null?void 0:A.hfd_px)??null:((da=s==null?void 0:s.aggregate)==null?void 0:da.median_hfd_px)??null,na=j?(A==null?void 0:A.fwhm_px)??null:((ua=s==null?void 0:s.aggregate)==null?void 0:ua.median_fwhm_px)??null,sa=j?(A==null?void 0:A.concentration)??null:((ma=s==null?void 0:s.aggregate)==null?void 0:ma.median_concentration)??null,Ae=Oe!=null&&ae!=null&&ae>0?(Oe-ae)/ae*100:null,oa=Se.slice(-3).map(t=>t.hfd),pt=Se.slice(-6,-3).map(t=>t.hfd),ht=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let De="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")De="cam.focus.guidance.noStars";else if(Se.length>=5&&Ae!=null&&Ae<=3)De="cam.focus.guidance.best";else if(oa.length===3&&pt.length===3){const t=ht(oa)-ht(pt),r=Math.max(.04,ht(pt)*.02);De=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const hr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Ge=Math.max(1,Math.ceil(pe.length/Ve)),ze=Math.min(ot,Ge),ia=(ze-1)*Ve,fr=pe.slice(ia,ia+Ve);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Mr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Gr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void Qa(t),className:`rounded border px-2 py-1 ${Dt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void Zt(!ke,_e),className:`rounded border px-2 py-1 ${ke?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void Zt(ke,!_e),className:`rounded border px-2 py-1 ${_e?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void tr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(kr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ar(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Br,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void er(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!0),className:`rounded border px-2 py-1 ${ta?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!1),className:`rounded border px-2 py-1 ${ta?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:tt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Lt,onChange:t=>Ut(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:at||!tt.trim(),onClick:()=>void rr(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Vt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Vt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",gt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:at,onClick:()=>void nr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:at,onClick:()=>void sr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.cpu_usage)==null?"—":`${Number(f.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.memory_usage)==null?"—":`${Number(f.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.temperature)==null?"—":`${Number(f.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>S?void dt():void Yt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${S?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(bt,{className:"mr-1 inline h-3.5 w-3.5"}),a(te?"cam.focus.starting":S?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(x?"cam.state.streaming":"cam.state.idle")]})]})]}),ye&&!x&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${S?"cursor-crosshair":""}`,onClick:Wa,children:[x&&ye&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),x?e.jsx("img",{ref:$,alt:"camera-preview",className:"h-full w-full object-contain",src:dr,onLoad:()=>{Z(null),Q(!1),lr()},onError:()=>{var r;if(fe(),!X.current)return;const t=(r=$.current)==null?void 0:r.src;(async()=>{if(!X.current||!t)return;if(await Pa()==="busy"){Z(a("cam.err.streamBusy")),Q(!0);return}Pe.current=window.setTimeout(()=>{X.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Fa,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"mr-1 inline h-3.5 w-3.5"}),a(T?"cam.btn.starting":"cam.btn.start")]})]}),x&&S&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Ra,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Ot/60).toString().padStart(2,"0")}:${(Ot%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Mt(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(Tt?"cam.hist.expand":"cam.hist.collapse")})}),!Tt&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Bt,onChange:t=>Ta(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ke,onChange:t=>Ma(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Xe,onChange:t=>Ia(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Pt,onChange:t=>Ha(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Ee,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:Ye.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:Ye.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[Ye.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${S?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(bt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${S?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(S?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),S?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[S?e.jsx("button",{type:"button",onClick:()=>void dt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>void Yt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(bt,{className:"mr-1 inline h-4 w-4"}),a(te?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!S,onClick:qa,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!S||j==null,onClick:La,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),S?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:Oe==null?"—":Oe.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(De)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:ae==null?"—":`${ae.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ae==null?"—":`+${Math.max(0,Ae).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(hr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:na==null?"—":`${na.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:sa==null?"—":`${(sa*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(zr,{samples:Se,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Aa).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((xa=n==null?void 0:n.info)==null?void 0:xa.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((pa=n==null?void 0:n.info)==null?void 0:pa.actual_exposure_us)!=null?gt((i==null?void 0:i.actual_exposure_us)??((ha=n==null?void 0:n.info)==null?void 0:ha.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(fa=n==null?void 0:n.info)!=null&&fa.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:ie(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Te.current!=null?`${Math.max(0,Math.round((performance.now()-Te.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ba=n==null?void 0:n.info)==null?void 0:ba.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ga=n==null?void 0:n.info)==null?void 0:ga.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((va=n==null?void 0:n.info)==null?void 0:va.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(ya=n==null?void 0:n.info)!=null&&ya.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ja=n==null?void 0:n.info)==null?void 0:ja.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((wa=n==null?void 0:n.info)==null?void 0:wa.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.width)??"—"}x${((Sa=n==null?void 0:n.info)==null?void 0:Sa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(ka=n==null?void 0:n.info)!=null&&ka.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!ra,onClick:()=>void Ua(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${ra?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Hr,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!xr,onClick:()=>void Va(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Fa,{className:"inline h-4 w-4"})," ",a(wt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!pr,onClick:()=>void Ja(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Ra,{className:"inline h-4 w-4"})," ",Je?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Jt,onClick:()=>void ut(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),za&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),_&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx($r,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:_.filename})]}),e.jsx("button",{type:"button",onClick:()=>mt(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(_r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ba(_.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:_.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(_.modified).toLocaleString()})]}),_.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:gt(_.exposure_us)})]}),_.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:_.analogue_gain})]}),_.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:_.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[pe.length===0&&!Jt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),fr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Fe===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ba(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ir(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Er,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void or(t.name),className:`rounded border px-2 py-1 ${Fe===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Pr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(wr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ze<=1,onClick:()=>Re(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:ze,total:Ge})}),e.jsx("button",{type:"button",disabled:ze>=Ge,onClick:()=>Re(t=>Math.min(Ge,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:St,onChange:t=>{kt(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:_t,onChange:t=>{Ct(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Et,onChange:t=>{$t(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Dr.map(t=>e.jsx("button",{type:"button",onClick:()=>{Rt(t),ee(!0)},className:`rounded border px-2 py-1 ${Ft===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!ue,onClick:()=>void Ka(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Ir,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Ie&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(G,{label:a("cam.controls.exposure"),value:u.exposureSeconds,min:1e-4,max:.5,step:1e-4,unit:" s",disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,exposureSeconds:t}))}}),e.jsx(G,{label:a("cam.controls.gain"),value:u.gain,min:1,max:24,step:.1,disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.digitalGain"),value:u.digitalGain,min:1,max:8,step:.1,disabled:Ie||!ea,onChange:t=>{w(!0),N(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:u.noiseReductionMode,onChange:t=>{w(!0),N(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:mr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(G,{label:a("cam.controls.contrast"),value:u.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.brightness"),value:u.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),N(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.saturation"),value:u.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.sharpness"),value:u.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),!ea&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((_a=n==null?void 0:n.info)==null?void 0:_a.actual_digital_gain)??u.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ce,onClick:()=>void Xa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:u.autoExposure?"auto":"manual",onChange:t=>{N(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:u.colorMode,onChange:t=>{N(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:u.whiteBalanceMode,onChange:t=>{N(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:ur.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:u.aeFlickerMode,onChange:t=>{N(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.5,step:.01,value:u.autoExposureMaxSeconds,onChange:t=>{N(r=>({...r,autoExposureMaxSeconds:k(Number(t.target.value)||.5,.01,.5)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(G,{label:"R Gain",value:u.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(G,{label:"B Gain",value:u.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!xt&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void Ya(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(le||de)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[le&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:le}),de&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:de})]})]})}vr.createRoot(document.getElementById("root")).render(e.jsx(yr.StrictMode,{children:e.jsx(Nr,{children:e.jsx(Fr,{children:e.jsx(qr,{})})})})); diff --git a/web/static/analysis-lab/camera.html b/web/static/analysis-lab/camera.html index 3df97a9..ea814ec 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,7 +4,7 @@ OGScope 相机调试控制台 - + From 7f9b32c77dcaf2ec404c6cf21fe698a49a0a9318 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: Thu, 27 Aug 2026 01:17:01 +0800 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20=E6=89=A9=E5=B1=95=E4=BC=A0?= =?UTF-8?q?=E6=84=9F=E5=99=A8=E8=A7=A3=E7=AE=97=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=20/=20extend=20sensor=20solve=20context=20co?= =?UTF-8?q?ntract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contracts/core-rest-v1.md | 4 ++++ docs/contracts/core-rest-v1_EN.md | 4 ++++ .../algorithms/plate_solve/sensor_context.py | 6 +++++ ogscope/web/api/models/schemas.py | 2 ++ tests/unit/test_core_contract_api.py | 22 +++++++++++++++++-- tests/unit/test_sensor_solve_context.py | 22 +++++++++++++++++++ 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/contracts/core-rest-v1.md b/docs/contracts/core-rest-v1.md index 4b0155d..220ffbb 100644 --- a/docs/contracts/core-rest-v1.md +++ b/docs/contracts/core-rest-v1.md @@ -23,6 +23,10 @@ - `fov_estimate` - `fov_max_error` - `solve_timeout_ms` + - `solve_context`(可选传感器上下文) + - `quality.time_fresh`:可选;显式为 `false` 时不使用该时间做传感器预测 + - `quality.camera_pose_calibrated`:可选;显式为 `false` 时不使用机械轴角做光轴预测 + - 两个字段缺失时保持旧客户端行为;字段为 `false` 只禁用传感器预测,不否决星图解算结果 - 响应: - `success: bool` - `session_id: str` diff --git a/docs/contracts/core-rest-v1_EN.md b/docs/contracts/core-rest-v1_EN.md index 5654143..f740908 100644 --- a/docs/contracts/core-rest-v1_EN.md +++ b/docs/contracts/core-rest-v1_EN.md @@ -23,6 +23,10 @@ This document defines the **minimal stable REST surface** for callers integratin - `fov_estimate` - `fov_max_error` - `solve_timeout_ms` + - `solve_context` (optional sensor context) + - `quality.time_fresh`: optional; when explicitly `false`, time is not used for sensor prediction + - `quality.camera_pose_calibrated`: optional; when explicitly `false`, mount angles are not used as camera-pose prediction + - Omitting both fields preserves legacy-client behavior; `false` disables sensor prediction only and never rejects a plate-solve result - Response: - `success: bool` - `session_id: str` diff --git a/ogscope/algorithms/plate_solve/sensor_context.py b/ogscope/algorithms/plate_solve/sensor_context.py index c86adab..54fbd20 100644 --- a/ogscope/algorithms/plate_solve/sensor_context.py +++ b/ogscope/algorithms/plate_solve/sensor_context.py @@ -148,8 +148,12 @@ def predict_from_solve_context( gps_valid = bool(quality.get("gps_valid")) time_valid = bool(quality.get("time_valid")) + time_fresh_raw = quality.get("time_fresh") + time_fresh = time_valid if time_fresh_raw is None else bool(time_fresh_raw) mount_valid = bool(quality.get("mount_valid")) heading_valid = bool(quality.get("heading_valid")) + camera_pose_raw = quality.get("camera_pose_calibrated") + camera_pose_calibrated = True if camera_pose_raw is None else bool(camera_pose_raw) lat = _optional_float(observer.get("latitude_deg")) lon = _optional_float(observer.get("longitude_deg")) when = _parse_utc(observer.get("time_utc")) @@ -160,6 +164,8 @@ def predict_from_solve_context( if ( not gps_valid or not time_valid + or not time_fresh + or not camera_pose_calibrated or lat is None or lon is None or when is None diff --git a/ogscope/web/api/models/schemas.py b/ogscope/web/api/models/schemas.py index d75756b..1c8c55b 100644 --- a/ogscope/web/api/models/schemas.py +++ b/ogscope/web/api/models/schemas.py @@ -226,8 +226,10 @@ class SolveContextQuality(BaseModel): gps_valid: bool = False time_valid: bool = False + time_fresh: Optional[bool] = None heading_valid: bool = False mount_valid: bool = False + camera_pose_calibrated: Optional[bool] = None class SolveContextPayload(BaseModel): diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index 99be413..c01b149 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -144,8 +144,10 @@ def test_core_analysis_lifecycle(client, monkeypatch) -> None: """开始-查询-结束分析生命周期 / Start-result-stop lifecycle.""" from ogscope.core.application import core_service + received: dict = {} + async def _fake_start(**kwargs): # noqa: ANN003 - _ = kwargs + received.update(kwargs) return {"success": True, "message": "started"} async def _fake_status(): @@ -164,9 +166,25 @@ async def _fake_stop(): monkeypatch.setattr(core_service.realtime_solve_service, "get_status", _fake_status) monkeypatch.setattr(core_service.realtime_solve_service, "stop", _fake_stop) - start = client.post("/api/core/v1/analysis/start", json={}) + start = client.post( + "/api/core/v1/analysis/start", + json={ + "solve_context": { + "quality": { + "gps_valid": True, + "time_valid": True, + "time_fresh": False, + "heading_valid": True, + "mount_valid": True, + "camera_pose_calibrated": False, + } + } + }, + ) assert start.status_code == 200 assert start.json()["state"] == "running" + assert received["solve_context"].quality.time_fresh is False + assert received["solve_context"].quality.camera_pose_calibrated is False result = client.get("/api/core/v1/analysis/result") assert result.status_code == 200 diff --git a/tests/unit/test_sensor_solve_context.py b/tests/unit/test_sensor_solve_context.py index 6a85fda..016b8bb 100644 --- a/tests/unit/test_sensor_solve_context.py +++ b/tests/unit/test_sensor_solve_context.py @@ -85,3 +85,25 @@ def test_sensor_prediction_unavailable_when_time_invalid() -> None: attach_sensor_prediction(row, context) assert row["sensor_prediction"]["sensor_status"] == "unavailable" + + +def test_sensor_prediction_unavailable_when_time_is_stale() -> None: + """过期观测时间不得参与预测 / Stale observation time cannot drive prediction.""" + context = _solve_context() + context["quality"]["time_fresh"] = False + row = {"status": "MATCH_FOUND", "ra_deg": 0.0, "dec_deg": 0.0} + attach_sensor_prediction(row, context) + + assert row["status"] == "MATCH_FOUND" + assert row["sensor_prediction"]["sensor_status"] == "unavailable" + + +def test_sensor_prediction_unavailable_without_camera_pose_calibration() -> None: + """未标定光轴不得参与预测 / Uncalibrated camera pose cannot drive prediction.""" + context = _solve_context() + context["quality"]["camera_pose_calibrated"] = False + row = {"status": "MATCH_FOUND", "ra_deg": 0.0, "dec_deg": 0.0} + attach_sensor_prediction(row, context) + + assert row["status"] == "MATCH_FOUND" + assert row["sensor_prediction"]["sensor_status"] == "unavailable" From 039d97e8776489880b17efb6e6a96bc3db457d53 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: Thu, 27 Aug 2026 15:53:25 +0800 Subject: [PATCH 13/24] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=9E=81?= =?UTF-8?q?=E8=BD=B4=E9=95=9C=E8=87=AA=E5=8A=A8=E6=9B=9D=E5=85=89=E7=AD=96?= =?UTF-8?q?=E7=95=A5=20/=20improve=20polar-scope=20auto=20exposure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/ogscope.env.example | 4 +- docs/DEBUG_CONSOLE.md | 2 +- docs/DEBUG_CONSOLE_EN.md | 2 +- ogscope/config.py | 14 ++-- ogscope/config_catalog.py | 1 + ogscope/platform/hardware/camera.py | 82 ++++++++++++++++++- ogscope/web/camera_shared.py | 3 +- tests/unit/test_camera_flip.py | 40 +++++++-- tests/unit/test_config_catalog.py | 6 +- web/spa/src/apps/camera/CameraConsoleApp.tsx | 10 +-- ...{camera-DU1oMy5v.js => camera-N0CSyGAW.js} | 2 +- web/static/analysis-lab/camera.html | 2 +- 12 files changed, 140 insertions(+), 28 deletions(-) rename web/static/analysis-lab/assets/{camera-DU1oMy5v.js => camera-N0CSyGAW.js} (99%) diff --git a/deploy/ogscope.env.example b/deploy/ogscope.env.example index 71a1ef7..63cef90 100644 --- a/deploy/ogscope.env.example +++ b/deploy/ogscope.env.example @@ -41,7 +41,9 @@ OGSCOPE_RELOAD=false # OGSCOPE_CAMERA_EXPOSURE=10000 # OGSCOPE_CAMERA_GAIN=1.0 # OGSCOPE_CAMERA_AE_POLAR_PRESET=true -# OGSCOPE_CAMERA_AE_EXPOSURE_VALUE=0.35 +# OGSCOPE_CAMERA_AE_EXPOSURE_VALUE=1.0 +# OGSCOPE_CAMERA_AE_AGGRESSIVE_ENABLED=true +# OGSCOPE_CAMERA_AUTO_EXPOSURE_MAX_US=600000 # OGSCOPE_CAMERA_FLIP_HORIZONTAL=false # OGSCOPE_CAMERA_FLIP_VERTICAL=false diff --git a/docs/DEBUG_CONSOLE.md b/docs/DEBUG_CONSOLE.md index 1568637..b7bf5ad 100644 --- a/docs/DEBUG_CONSOLE.md +++ b/docs/DEBUG_CONSOLE.md @@ -238,7 +238,7 @@ python scripts/test_debug_console.py --test deps | `camera_white_balance_mode` | `auto` | `auto` / `manual` / `night` | | `camera_white_balance_gain_r` / `camera_white_balance_gain_b` | `1.0` | 手动白平衡红/蓝增益 | | `camera_night_mode` | `false` | 启动时应用夜间白平衡标记 | -| `camera_auto_exposure_max_us` | `500000` | 自动曝光最长帧周期,暗场最多降至约 2 fps | +| `camera_auto_exposure_max_us` | `600000` | 自动曝光最长帧周期,暗场允许降至约 1.7 fps | | `camera_ae_flicker_mode` | `off` | `off` / `50hz` / `60hz` | | `camera_noise_reduction_mode` | `fast` | `off` / `fast` / `high_quality` | | `camera_lores_enabled` | `true` | 启用低分辨率辅助流统计 | diff --git a/docs/DEBUG_CONSOLE_EN.md b/docs/DEBUG_CONSOLE_EN.md index ccd17d0..53a882e 100644 --- a/docs/DEBUG_CONSOLE_EN.md +++ b/docs/DEBUG_CONSOLE_EN.md @@ -188,7 +188,7 @@ These settings enter runtime through environment variables or config files. Name | `camera_white_balance_mode` | `auto` | `auto` / `manual` / `night` | | `camera_white_balance_gain_r` / `camera_white_balance_gain_b` | `1.0` | Manual white-balance red/blue gains | | `camera_night_mode` | `false` | Apply night white-balance flag at startup | -| `camera_auto_exposure_max_us` | `500000` | Longest AE frame duration; dark scenes may fall to about 2 fps | +| `camera_auto_exposure_max_us` | `600000` | Longest AE frame duration; dark scenes may fall to about 1.7 fps | | `camera_ae_flicker_mode` | `off` | `off` / `50hz` / `60hz` | | `camera_noise_reduction_mode` | `fast` | `off` / `fast` / `high_quality` | | `camera_lores_enabled` | `true` | Enable the low-resolution helper stream | diff --git a/ogscope/config.py b/ogscope/config.py index 967a314..39d0fbd 100644 --- a/ogscope/config.py +++ b/ogscope/config.py @@ -105,16 +105,20 @@ class Settings(BaseSettings): description="自动曝光时启用电子极轴镜 AE 预设 (Shadows/Matrix/Long+EV) / AE polar-scope preset", ) camera_ae_exposure_value: float = Field( - default=0.35, + default=1.0, ge=-2.0, le=2.0, description="AE 曝光补偿(档),与 camera_ae_polar_preset 联用 / AE exposure comp EV stops", ) + camera_ae_aggressive_enabled: bool = Field( + default=True, + description="根据暗部与高光占比动态提高极轴镜 AE / Dynamically bias polar AE toward dark detail", + ) camera_auto_exposure_max_us: int = Field( - default=500_000, + default=600_000, ge=10_000, le=10_000_000, - description="自动曝光最长帧周期 500ms,暗场允许降帧 / Max auto-exposure frame duration, capped at 500ms", + description="自动曝光最长帧周期 600ms,暗场允许降帧 / Max auto-exposure frame duration, capped at 600ms", ) camera_ae_flicker_mode: str = Field( default="off", @@ -536,9 +540,9 @@ def _parse_camera_white_balance_mode(cls, value: object) -> str: @field_validator("camera_auto_exposure_max_us", mode="before") @classmethod def _cap_camera_auto_exposure_max_us(cls, value: object) -> object: - """兼容旧配置并限制暗场曝光为 500ms / Keep legacy config bootable and cap AE at 500ms.""" + """兼容旧配置并限制暗场曝光为 600ms / Keep legacy config bootable and cap AE at 600ms.""" try: - return min(500_000, int(value)) + return min(600_000, int(value)) except (TypeError, ValueError): return value diff --git a/ogscope/config_catalog.py b/ogscope/config_catalog.py index f95cb6b..28c7307 100644 --- a/ogscope/config_catalog.py +++ b/ogscope/config_catalog.py @@ -68,6 +68,7 @@ "camera_gain", "camera_ae_polar_preset", "camera_ae_exposure_value", + "camera_ae_aggressive_enabled", "camera_auto_exposure_max_us", "camera_ae_flicker_mode", "camera_noise_reduction_mode", diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 6cd2ea9..4fd9910 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -5,6 +5,7 @@ """ import logging +import time from abc import ABC, abstractmethod from concurrent.futures import TimeoutError as FutureTimeoutError from typing import Any, Optional @@ -61,6 +62,8 @@ class IMX327MIPICamera(CameraInterface): SENSOR_MAX_WIDTH = 1920 SENSOR_MAX_HEIGHT = 1020 PREVIEW_BUFFER_COUNT = 2 + AE_SUPERVISOR_INTERVAL_S = 1.0 + AE_SUPERVISOR_STEP_EV = 0.25 MANUAL_CONTROL_RANGE_DEFAULTS = { "ExposureTime": {"min": 1000, "max": 100000, "default": 10000, "step": 1000}, "AnalogueGain": {"min": 1.0, "max": 16.0, "default": 1.0, "step": 0.1}, @@ -103,7 +106,7 @@ def __init__(self, config: dict[str, Any]): self.white_balance_gain_b = config.get("white_balance_gain_b", 1.0) self.night_mode = bool(config.get("night_mode", False)) self.auto_exposure_max_us = max( - 10_000, min(500_000, int(config.get("auto_exposure_max_us", 500_000))) + 10_000, min(600_000, int(config.get("auto_exposure_max_us", 600_000))) ) self.capture_timeout_sec = max( 0.5, float(config.get("capture_timeout_sec", 4.0)) @@ -130,7 +133,10 @@ def __init__(self, config: dict[str, Any]): # 电子极轴镜默认 AE 策略(约 16mm 广角、低帧率夜空);仍由 libcamera ISP 闭环 / Polar-scope AE defaults (16mm, dark sky; ISP AE loop). self.ae_polar_preset = bool(config.get("ae_polar_preset", True)) - self.ae_exposure_value = float(config.get("ae_exposure_value", 0.35)) + self.ae_exposure_value = float(config.get("ae_exposure_value", 1.0)) + self.ae_aggressive_enabled = bool(config.get("ae_aggressive_enabled", True)) + self._ae_effective_exposure_value = self.ae_exposure_value + self._ae_last_adjust_at = 0.0 logger.info( f"初始化 IMX327 MIPI 相机: {self.width}x{self.height}@{self.fps}fps" @@ -506,14 +512,78 @@ def _collect_lores_stats(self, request: Any) -> None: y_plane = lores[..., 0] else: y_plane = lores + p50, p90, p99 = np.percentile(y_plane, [50, 90, 99]) + saturated_fraction = float(np.mean(y_plane >= 250)) self._last_lores_stats = { "mean": float(np.mean(y_plane)), "min": int(np.min(y_plane)), "max": int(np.max(y_plane)), + "p50": float(p50), + "p90": float(p90), + "p99": float(p99), + "saturated_fraction": saturated_fraction, } + self._update_aggressive_auto_exposure(self._last_lores_stats) except Exception as e: logger.debug("读取 lores 统计失败 / Failed to read lores stats: %s", e) + def _desired_aggressive_ae_ev(self, stats: dict[str, Any]) -> float: + """按暗部与高光占比选择 AE 补偿 / Select AE bias from shadows and highlight share.""" + p50 = float(stats.get("p50") or 0.0) + saturated = float(stats.get("saturated_fraction") or 0.0) + base = max(-2.0, min(2.0, float(self.ae_exposure_value))) + + # A tiny clipped lamp must not black out the rest of the scene. Allow up + # to 2% clipping and aggressively lift a dark median; broad daylight or + # widespread clipping falls back to neutral compensation. + # 少量过曝路灯不应压黑整幅画面;允许最多 2% 高光裁切并积极抬升暗部中位数, + # 白天或大面积过曝时则回落到中性补偿。 + if p50 >= 180.0 or saturated >= 0.20: + return 0.0 + if p50 <= 40.0 and saturated <= 0.02: + return max(base, 1.5) + if p50 <= 80.0 and saturated <= 0.05: + return max(base, 1.0) + if p50 <= 120.0 and saturated <= 0.08: + return max(0.5, min(base, 1.0)) + return min(base, 0.5) + + def _update_aggressive_auto_exposure(self, stats: dict[str, Any]) -> None: + """慢速调整 libcamera EV,避免与底层 AE 抢控制 / Slowly supervise libcamera EV.""" + if ( + not self.camera + or not self.auto_exposure + or not self.ae_polar_preset + or not self.ae_aggressive_enabled + ): + return + now = time.monotonic() + if now - self._ae_last_adjust_at < self.AE_SUPERVISOR_INTERVAL_S: + return + desired = self._desired_aggressive_ae_ev(stats) + current = float(self._ae_effective_exposure_value) + delta = desired - current + if abs(delta) < 0.1: + self._ae_last_adjust_at = now + return + step = min(abs(delta), self.AE_SUPERVISOR_STEP_EV) + next_ev = current + (step if delta > 0 else -step) + controls = self._camera_controls() + try: + if "ExposureValue" in controls: + self.camera.set_controls({"ExposureValue": next_ev}) + elif "Brightness" in controls: + self.camera.set_controls( + {"Brightness": max(-1.0, min(1.0, next_ev * 0.2))} + ) + else: + return + except Exception as exc: + logger.debug("动态 AE 补偿未生效 / Dynamic AE bias failed: %s", exc) + return + self._ae_effective_exposure_value = next_ev + self._ae_last_adjust_at = now + def _camera_capabilities(self) -> dict[str, Any]: """汇总相机能力供 API/UI 降级 / Summarize camera capabilities for API/UI fallback.""" cc = self._camera_controls() @@ -569,7 +639,7 @@ def _apply_polar_auto_exposure_controls(self) -> None: updates["AeMeteringMode"] = pcc.AeMeteringModeEnum.Matrix if hasattr(pcc, "AeExposureModeEnum"): updates["AeExposureMode"] = pcc.AeExposureModeEnum.Long - ev = float(self.ae_exposure_value) + ev = float(self._ae_effective_exposure_value) if abs(ev) > 1e-6: if "ExposureValue" in cc: updates["ExposureValue"] = ev @@ -1017,6 +1087,8 @@ def set_auto_exposure(self, enabled: bool) -> bool: try: self.auto_exposure = enabled if enabled: + self._ae_effective_exposure_value = self.ae_exposure_value + self._ae_last_adjust_at = 0.0 self._apply_polar_auto_exposure_controls() else: self.camera.set_controls({"AeEnable": False}) @@ -1198,6 +1270,8 @@ def get_camera_info(self) -> dict[str, Any]: "night_mode": self.night_mode, "ae_polar_preset": self.ae_polar_preset, "ae_exposure_value": self.ae_exposure_value, + "ae_effective_exposure_value": self._ae_effective_exposure_value, + "ae_aggressive_enabled": self.ae_aggressive_enabled, "lores_enabled": self.lores_enabled, "lores_available": self._lores_available, "lores_width": self.lores_width, @@ -1320,7 +1394,7 @@ def set_auto_exposure_max_us(self, value: int) -> bool: if not self.is_initialized: logger.error("相机未初始化") return False - self.auto_exposure_max_us = max(10_000, min(500_000, int(value))) + self.auto_exposure_max_us = max(10_000, min(600_000, int(value))) self._apply_frame_duration_controls() return True diff --git a/ogscope/web/camera_shared.py b/ogscope/web/camera_shared.py index 73c70d8..9ae8e6e 100644 --- a/ogscope/web/camera_shared.py +++ b/ogscope/web/camera_shared.py @@ -116,8 +116,9 @@ def _build_base_config(self) -> dict[str, Any]: "auto_exposure": True, "ae_polar_preset": settings.camera_ae_polar_preset, "ae_exposure_value": settings.camera_ae_exposure_value, + "ae_aggressive_enabled": settings.camera_ae_aggressive_enabled, "auto_exposure_max_us": getattr( - settings, "camera_auto_exposure_max_us", 500_000 + settings, "camera_auto_exposure_max_us", 600_000 ), "capture_timeout_sec": self._capture_timeout_sec, "ae_flicker_mode": getattr(settings, "camera_ae_flicker_mode", "off"), diff --git a/tests/unit/test_camera_flip.py b/tests/unit/test_camera_flip.py index a463cd4..c0722e3 100644 --- a/tests/unit/test_camera_flip.py +++ b/tests/unit/test_camera_flip.py @@ -220,20 +220,50 @@ def test_preview_encoder_falls_back_to_opencv_when_turbojpeg_missing( @pytest.mark.unit def test_frame_duration_limits_allow_long_auto_exposure() -> None: cam = IMX327MIPICamera( - _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=500_000) + _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=600_000) ) - assert cam._compute_frame_duration_limits() == (125_000, 500_000) + assert cam._compute_frame_duration_limits() == (125_000, 600_000) @pytest.mark.unit -def test_auto_exposure_ceiling_is_capped_at_half_second() -> None: +def test_auto_exposure_ceiling_is_capped_at_six_tenths_second() -> None: cam = IMX327MIPICamera( _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=2_000_000) ) - assert cam.auto_exposure_max_us == 500_000 - assert cam._compute_frame_duration_limits() == (125_000, 500_000) + assert cam.auto_exposure_max_us == 600_000 + assert cam._compute_frame_duration_limits() == (125_000, 600_000) + + +@pytest.mark.unit +def test_aggressive_ae_ignores_small_clipped_highlight() -> None: + fake = _FakePicamera2() + fake.camera_controls = {"ExposureValue": (-2.0, 2.0, 0.0)} + cam = IMX327MIPICamera(_minimal_config(auto_exposure=True, ae_exposure_value=1.0)) + cam.camera = fake + + cam._update_aggressive_auto_exposure( + {"p50": 20.0, "p90": 35.0, "p99": 80.0, "saturated_fraction": 0.01} + ) + + assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(1.25) + assert cam._ae_effective_exposure_value == pytest.approx(1.25) + + +@pytest.mark.unit +def test_aggressive_ae_returns_toward_neutral_in_bright_scene() -> None: + fake = _FakePicamera2() + fake.camera_controls = {"ExposureValue": (-2.0, 2.0, 0.0)} + cam = IMX327MIPICamera(_minimal_config(auto_exposure=True, ae_exposure_value=1.0)) + cam.camera = fake + + cam._update_aggressive_auto_exposure( + {"p50": 220.0, "p90": 250.0, "p99": 255.0, "saturated_fraction": 0.25} + ) + + assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(0.75) + assert cam._ae_effective_exposure_value == pytest.approx(0.75) @pytest.mark.unit diff --git a/tests/unit/test_config_catalog.py b/tests/unit/test_config_catalog.py index d2b8e59..ac7844e 100644 --- a/tests/unit/test_config_catalog.py +++ b/tests/unit/test_config_catalog.py @@ -41,8 +41,8 @@ def test_simulation_mode_tri_state(raw: str | None, expected: bool | None) -> No @pytest.mark.unit -def test_legacy_auto_exposure_ceiling_is_capped_at_half_second() -> None: - """旧环境值不阻止启动且会收敛到 0.5 秒 / Legacy values boot and clamp to 0.5s.""" +def test_legacy_auto_exposure_ceiling_is_capped_at_six_tenths_second() -> None: + """旧环境值不阻止启动且会收敛到 0.6 秒 / Legacy values boot and clamp to 0.6s.""" settings = Settings(camera_auto_exposure_max_us=2_000_000) - assert settings.camera_auto_exposure_max_us == 500_000 + assert settings.camera_auto_exposure_max_us == 600_000 diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index 0c860e9..67cb623 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -428,7 +428,7 @@ export function CameraConsoleApp() { noiseReduction: 0, noiseReductionMode: "fast", aeFlickerMode: "off", - autoExposureMaxSeconds: 0.5, + autoExposureMaxSeconds: 0.6, whiteBalanceMode: "auto", whiteBalanceGainR: 1.0, whiteBalanceGainB: 1.0, @@ -767,7 +767,7 @@ export function CameraConsoleApp() { if (!info) return; const syncRuntime = options?.syncRuntime ?? true; setForm({ - exposureSeconds: clamp(exposureUsToSeconds(info.exposure_us, 5000), 0.0001, 0.5), + exposureSeconds: clamp(exposureUsToSeconds(info.exposure_us, 5000), 0.0001, 0.6), gain: clamp(toNum(info.analogue_gain, 1.0), 1.0, 24.0), digitalGain: clamp(toNum(info.digital_gain, 1.0), 1.0, 8.0), autoExposure: Boolean(info.auto_exposure ?? true), @@ -778,7 +778,7 @@ export function CameraConsoleApp() { noiseReduction: clamp(Math.round(toNum(info.noise_reduction, 0)), 0, 4), noiseReductionMode: String(info.noise_reduction_mode ?? "fast"), aeFlickerMode: String(info.ae_flicker_mode ?? "off"), - autoExposureMaxSeconds: clamp(exposureUsToSeconds(info.auto_exposure_max_us, 500000), 0.01, 0.5), + autoExposureMaxSeconds: clamp(exposureUsToSeconds(info.auto_exposure_max_us, 600000), 0.01, 0.6), whiteBalanceMode: String(info.white_balance_mode ?? "auto"), whiteBalanceGainR: clamp(toNum(info.white_balance_gain_r, 1.0), 0.1, 3.0), whiteBalanceGainB: clamp(toNum(info.white_balance_gain_b, 1.0), 0.1, 3.0), @@ -2141,7 +2141,7 @@ export function CameraConsoleApp() { )} {exposureLocked &&

{t("cam.controls.lockedByAe")}

}
- { setFormDirty(true); setForm((p) => ({ ...p, exposureSeconds: v })); }} /> + { setFormDirty(true); setForm((p) => ({ ...p, exposureSeconds: v })); }} /> { setFormDirty(true); setForm((p) => ({ ...p, gain: Number(v.toFixed(1)) })); }} /> { setFormDirty(true); setForm((p) => ({ ...p, digitalGain: Number(v.toFixed(1)) })); }} /> { setForm((p) => ({ ...p, whiteBalanceGainR: Number(v.toFixed(1)) })); setFormDirty(true); }} /> { setForm((p) => ({ ...p, whiteBalanceGainB: Number(v.toFixed(1)) })); setFormDirty(true); }} /> diff --git a/web/static/analysis-lab/assets/camera-DU1oMy5v.js b/web/static/analysis-lab/assets/camera-N0CSyGAW.js similarity index 99% rename from web/static/analysis-lab/assets/camera-DU1oMy5v.js rename to web/static/analysis-lab/assets/camera-N0CSyGAW.js index 29438ca..ae5b85d 100644 --- a/web/static/analysis-lab/assets/camera-DU1oMy5v.js +++ b/web/static/analysis-lab/assets/camera-N0CSyGAW.js @@ -58,4 +58,4 @@ import{r as o,j as e,a as vr,R as yr}from"./client-D1ZVDB-N.js";import{c as P,u * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Or=P("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Ar="/api/dev/debug";function H(a){return`${Ar}${a}`}async function h(a,b={}){const g=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Cr(g,b)}const Dr=["640x360","1280x720","1600x900","1920x1020"],Gr=[0,90,180,270],Ve=12;function k(a,b,g){return Math.min(g,Math.max(b,a))}function F(a,b){return a==null||Number.isNaN(Number(a))?b:Number(a)}function ie(a,b){return F(a,b)/1e6}function vt(a){return Math.round(a*1e6)}function gt(a){return`${ie(a,0).toFixed(4)} s`}function $a(a){const{exposureSeconds:b,autoExposureMaxSeconds:g,...f}=a;return{...f,exposure:vt(b),autoExposureMaxUs:vt(g)}}function Ba(a){if(!a)return"0 B";const b=["B","KB","MB","GB"];let g=0,f=a;for(;f>=1024&&gq.hfd),f=Math.min(...g),n=Math.max(...g),ce=Math.max(.2,n-f),x=g.map((q,K)=>{const le=K/Math.max(1,g.length-1)*320,l=76-(q-f)/ce*60;return`${le.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:x,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((z=x.split(" ").at(-1))==null?void 0:z.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Pa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const b=await a.json(),g=Number(b.max_clients??0),f=Number(b.active_clients??0);return g>0&&f>=g?"busy":"ok"}catch{return"fail"}}function G({label:a,value:b,min:g,max:f,step:n,onChange:ce,disabled:x=!1,unit:z=""}){const q=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${x?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(q),z]})]}),e.jsx("input",{type:"range",min:g,max:f,step:n,value:b,disabled:x,onChange:K=>ce(Number(K.target.value)),className:"w-full accent-primary"})]})}function qr(){var ca,la,da,ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a;const{t:a,locale:b,setLocale:g}=jr(),{info:f}=Sr(),[n,ce]=o.useState(null),[x,z]=o.useState(!1),[q,K]=o.useState(()=>Date.now()),[le,l]=o.useState(null),[ye,Z]=o.useState(null),[yt,Q]=o.useState(!1),[de,v]=o.useState(null),[T,je]=o.useState(!1),[Je,jt]=o.useState(!1),[wt,Nt]=o.useState(!1),[St,kt]=o.useState("8"),[_t,Ct]=o.useState("8"),[Ft,Rt]=o.useState("1280x720"),[Et,$t]=o.useState("supersample"),[ue,ee]=o.useState(!1),[Bt,Ta]=o.useState(!0),[Ke,Ma]=o.useState(!0),[Xe,Ia]=o.useState(!1),[Pt,Ha]=o.useState(!1),[Tt,Mt]=o.useState(!1),[Ye,Oa]=o.useState({mean:0,std:0,over:0}),[Aa,we]=o.useState(0),[i,Ze]=o.useState(null),[S,Qe]=o.useState(!1),[te,Ne]=o.useState(!1),[s,It]=o.useState(null),[Se,me]=o.useState([]),[ae,xe]=o.useState(null),[j,et]=o.useState(null),[Ht,O]=o.useState(null),[Ot,At]=o.useState(0),[Dt,Gt]=o.useState(180),[ke,zt]=o.useState(!1),[_e,qt]=o.useState(!1),[u,N]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.5,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ce,w]=o.useState(!1),[tt,Wt]=o.useState(""),[Lt,Ut]=o.useState(""),[Vt,Da]=o.useState([]),[at,W]=o.useState(!1),[pe,Ga]=o.useState([]),[Jt,Kt]=o.useState(!1),[_,rt]=o.useState(null),[za,nt]=o.useState(!1),[Fe,st]=o.useState(null),[ot,Re]=o.useState(1),$=o.useRef(null),Ee=o.useRef(null),he=o.useRef(null),$e=o.useRef(null),Be=o.useRef(null),L=o.useRef(null),Pe=o.useRef(null),it=o.useRef(!1),re=o.useRef(!1),X=o.useRef(!1),Te=o.useRef(null),U=o.useRef([]),ne=o.useRef(0),ct=o.useRef(null),Xt=()=>{U.current=[],ne.current=0,Te.current=null,we(0)},R=async()=>{try{const t=await h("/api/debug/camera/status",{cache:"no-store"});ce(t),t.streaming||(z(!1),X.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},fe=()=>{Pe.current!=null&&(window.clearTimeout(Pe.current),Pe.current=null)},lt=async()=>{if(T)return!1;je(!0),l(null),Z(null),Q(!1);try{fe(),n!=null&&n.streaming||await h("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Pa();return r==="busy"?(Z(a("cam.err.streamBusy")),Q(!0),!1):r==="fail"?(Z(a("cam.err.streamProbeFailed")),Q(!1),!1):(z(!0),X.current=!0,v(a("cam.notice.previewStart")),Xt(),Te.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{je(!1)}},Yt=async()=>{if(!te){Ne(!0),O(null);try{if(!(x||await lt()))return;const r=await h(H("/camera/focus/session/start"),{method:"POST"});re.current=r.active,N(c=>({...c,autoExposure:!1,exposureSeconds:ie(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),It(null),me([]),xe(null),et(null),Mt(!0),Qe(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},dt=async()=>{if(!te){Ne(!0),Qe(!1),O(null);try{if(re.current){const t=await h(H("/camera/focus/session/stop"),{method:"POST"});re.current=!1,t.restored_settings&&(N(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:ie(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},qa=()=>{me([]),xe(null),O(null)},Wa=t=>{if(!S||!$.current||t.target.closest("button, label, input"))return;const r=$.current,c=t.currentTarget.getBoundingClientRect(),p=r.naturalWidth||(s==null?void 0:s.frame.width)||0,d=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(p<=0||d<=0)return;const m=Math.min(c.width/p,c.height/d),C=p*m,y=d*m,E=(c.width-C)/2,B=(c.height-y)/2,D=(t.clientX-c.left-E)/C,se=(t.clientY-c.top-B)/y;D<0||D>1||se<0||se>1||(et({x:D,y:se}),me([]),xe(null),O(null))},La=()=>{et(null),me([]),xe(null),O(null)},Ua=async()=>{if(!T){je(!0),l(null);try{fe(),z(!1),X.current=!1,Z(null),Q(!1),Xt(),$.current&&($.current.onload=null,$.current.onerror=null,$.current.src="",$.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{je(!1)}}},Va=async()=>{if(!(!x&&!(n!=null&&n.streaming))){Nt(!0),l(null);try{const t=await h("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Nt(!1)}}},Ja=async()=>{if(!Je){jt(!0),l(null);try{if(n!=null&&n.recording)await h("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await h("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{jt(!1)}}},Ka=async()=>{l(null);try{const t=k(parseInt(St,10)||5,1,60);await h(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=k(parseInt(_t,10)||8,1,30);await h(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,p]=Ft.split("x").map(d=>parseInt(d,10));c&&p&&await h(`/api/debug/camera/size?width=${c}&height=${p}`,{method:"POST"}),await h(`/api/debug/camera/sampling?mode=${encodeURIComponent(Et)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),ee(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Xa=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ya=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Za=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;N({exposureSeconds:k(ie(t.exposure_us,5e3),1e-4,.5),gain:k(F(t.analogue_gain,1),1,24),digitalGain:k(F(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:k(F(t.contrast,1),0,2),brightness:k(F(t.brightness,0),-1,1),saturation:k(F(t.saturation,1),0,2),sharpness:k(F(t.sharpness,1),0,2),noiseReduction:k(Math.round(F(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:k(ie(t.auto_exposure_max_us,5e5),.01,.5),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:k(F(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:k(F(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(kt(String(Math.round(F(t.fps,8)))),Rt(`${Math.round(F(t.width,1280))}x${Math.round(F(t.height,720))}`),$t(String(t.sampling_mode??"supersample")),ee(!1)),Gt(k(Math.round(F(t.rotation,180)),0,270)),zt(!!t.flip_horizontal),qt(!!t.flip_vertical),w(!1)},Zt=async(t,r)=>{l(null);try{await h("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),zt(t),qt(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},Qa=async t=>{l(null);try{await h(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Gt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},Qt=async t=>{l(null);try{await h(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},er=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await h("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},tr=async()=>{l(null);try{await h("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},ar=async()=>{l(null);try{await h("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Me=async()=>{W(!0);try{const t=await h("/api/debug/camera/presets",{cache:"no-store"});Da(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{W(!1)}},rr=async()=>{const t=tt.trim();if(t){W(!0),l(null);try{await h("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Lt.trim(),exposure_us:vt(u.exposureSeconds),analogue_gain:u.gain,digital_gain:u.digitalGain,auto_exposure:u.autoExposure,contrast:u.contrast,brightness:u.brightness,saturation:u.saturation,sharpness:u.sharpness,noise_reduction:u.noiseReduction,white_balance_mode:u.whiteBalanceMode,white_balance_gain_r:u.whiteBalanceGainR,white_balance_gain_b:u.whiteBalanceGainB,rotation:Dt,flip_horizontal:ke,flip_vertical:_e,color_mode:u.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Ut(""),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},nr=async t=>{W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}},sr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},ut=async()=>{Kt(!0);try{const t=await h("/api/debug/files",{cache:"no-store"});Ga(t.files??[]),Re(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Kt(!1)}},mt=()=>{st(null),rt(null),nt(!1)},or=async t=>{if(Fe===t){mt();return}st(t),rt(null),nt(!0),l(null);try{const r=await h(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});rt(r)}catch(r){l(r instanceof Error?r.message:String(r)),st(null)}finally{nt(!1)}},ir=t=>{const r=(p,d)=>{const m=document.createElement("a");m.href=d,m.download=p,document.body.appendChild(m),m.click(),document.body.removeChild(m)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const d=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(d)}`)).ok)return;r(d,`${H("/files")}/${encodeURIComponent(d)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:d}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},cr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await h(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Fe===t||(_==null?void 0:_.filename)===t)&&mt(),await ut()}catch(r){l(r instanceof Error?r.message:String(r))}}},lr=()=>{if(!Bt||!$.current||!Ee.current)return;const t=$.current;if(!t.naturalWidth||!t.naturalHeight||(he.current||(he.current=document.createElement("canvas"),$e.current=he.current.getContext("2d",{willReadFrequently:!0})),Be.current||(Be.current=Ee.current.getContext("2d")),!$e.current||!Be.current))return;const c=Math.min(1,320/t.naturalWidth),p=Math.max(1,Math.round(t.naturalWidth*c)),d=Math.max(1,Math.round(t.naturalHeight*c));he.current.width=p,he.current.height=d,$e.current.drawImage(t,0,0,p,d);const m=$e.current.getImageData(0,0,p,d).data,C=new Array(256).fill(0),y=new Array(256).fill(0),E=new Array(256).fill(0),B=new Array(256).fill(0);let D=0,se=0,Ca=0;const oe=p*d;for(let I=0;I=250&&(Ca+=1)}const ft=oe?D/oe:0,br=oe?se/oe-ft*ft:0;Oa({mean:ft,std:Math.sqrt(Math.max(0,br)),over:oe?Ca/oe*100:0});const qe=Ee.current,M=Be.current,We=window.devicePixelRatio||1,be=Math.max(1,qe.clientWidth||240),ge=Math.max(1,qe.clientHeight||120);qe.width=Math.floor(be*We),qe.height=Math.floor(ge*We),M.setTransform(We,0,0,We,0,0),M.clearRect(0,0,be,ge);const gr=Math.max(1,...Ke?[Math.max(...C),Math.max(...y),Math.max(...E)]:[0],...Xe?[Math.max(...B)]:[0]),Le=(I,Ue)=>{M.beginPath(),M.strokeStyle=Ue,M.lineWidth=1.2;for(let V=0;V<256;V+=1){const ve=V/255*be,J=ge-I[V]/gr*ge;V===0?M.moveTo(ve,J):M.lineTo(ve,J)}M.stroke()};if(Ke&&(Le(C,"rgba(255,80,80,0.85)"),Le(y,"rgba(80,255,80,0.85)"),Le(E,"rgba(80,160,255,0.85)")),Xe&&Le(B,"rgba(255,255,255,0.95)"),Pt){const I=.9803921568627451*be;M.fillStyle="rgba(255,100,100,0.12)",M.fillRect(I,0,be-I,ge)}};o.useEffect(()=>{R(),Me(),ut();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ce||Za(n.info,{syncRuntime:!ue})},[n==null?void 0:n.info,Ce,ue]),o.useEffect(()=>{if(!(n!=null&&n.recording)){L.current&&(window.clearInterval(L.current),L.current=null),At(0);return}if(L.current)return;const t=Date.now();return L.current=window.setInterval(()=>{At(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{L.current&&(window.clearInterval(L.current),L.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{X.current=x,x||(fe(),Qe(!1),re.current&&dt())},[x]),o.useEffect(()=>()=>{re.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),re.current=!1)},[]),o.useEffect(()=>{if(!S||!x)return;let t=!1;const r=async()=>{var p,d;if(!(it.current||document.hidden)){it.current=!0;try{const m=new URLSearchParams;j&&(m.set("target_x",j.x.toFixed(6)),m.set("target_y",j.y.toFixed(6)));const C=m.size>0?`?${m.toString()}`:"",y=await h(`${H("/camera/focus/metrics")}${C}`,{cache:"no-store"});if(t)return;It(y),O(null);const E=j?(p=y.selected_star)==null?void 0:p.hfd_px:(d=y.aggregate)==null?void 0:d.median_hfd_px;typeof E=="number"&&Number.isFinite(E)&&(me(B=>{var D;return((D=B.at(-1))==null?void 0:D.frameId)===y.frame_id?B:[...B,{frameId:y.frame_id,hfd:E}].slice(-60)}),xe(B=>B==null?E:Math.min(B,E)))}catch(m){t||O(m instanceof Error?m.message:String(m))}finally{it.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[S,x,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{x&&(U.current=[],ne.current=0)},[q,x]),o.useEffect(()=>{if(!x){U.current=[],ne.current=0,we(0);return}if(!ct.current){const d=document.createElement("canvas");d.width=32,d.height=32,ct.current=d}const r=ct.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const p=()=>{const d=$.current;if(d!=null&&d.complete&&d.naturalWidth>0)try{r.drawImage(d,0,0,32,32);const m=r.getImageData(0,0,32,32).data;let C=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[x]),o.useEffect(()=>{if(!x){we(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,p=U.current;for(;p.length&&p[0]window.clearInterval(t)},[x]),o.useEffect(()=>{if(!x){Ze(null);return}let t=!1;const r=async()=>{try{const p=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!p.ok||t)return;const d=await p.json();if(!t){Ze(d);const m=Number(d.preview_target_fps??d.target_preview_fps??0);Number.isFinite(m)&&m>0&&!ue&&Ct(String(Math.round(m)))}}catch{t||Ze(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[x,ue]),o.useEffect(()=>{if(!de)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[de]),o.useEffect(()=>()=>fe(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(pe.length/Ve));ot>t&&Re(t)},[pe.length,ot]);const dr=x?`${H("/camera/stream")}?t=${q}`:"",Ie=u.autoExposure,xt=u.whiteBalanceMode==="manual",Y=((ca=n==null?void 0:n.info)==null?void 0:ca.capabilities)??{},ur=Array.isArray(Y.awb_modes)&&Y.awb_modes.length>0?Y.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],mr=Array.isArray(Y.noise_reduction_modes)&&Y.noise_reduction_modes.length>0?Y.noise_reduction_modes:["off","fast","high_quality"],ea=Y.manual_digital_gain!==!1,ta=!!((la=n==null?void 0:n.info)!=null&&la.night_mode),aa=x,He=!T&&!x&&!(n!=null&&n.recording),ra=!T&&x,xr=!T&&!wt&&aa,pr=!T&&!Je&&aa,A=j?s==null?void 0:s.selected_star:null,Oe=j?(A==null?void 0:A.hfd_px)??null:((da=s==null?void 0:s.aggregate)==null?void 0:da.median_hfd_px)??null,na=j?(A==null?void 0:A.fwhm_px)??null:((ua=s==null?void 0:s.aggregate)==null?void 0:ua.median_fwhm_px)??null,sa=j?(A==null?void 0:A.concentration)??null:((ma=s==null?void 0:s.aggregate)==null?void 0:ma.median_concentration)??null,Ae=Oe!=null&&ae!=null&&ae>0?(Oe-ae)/ae*100:null,oa=Se.slice(-3).map(t=>t.hfd),pt=Se.slice(-6,-3).map(t=>t.hfd),ht=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let De="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")De="cam.focus.guidance.noStars";else if(Se.length>=5&&Ae!=null&&Ae<=3)De="cam.focus.guidance.best";else if(oa.length===3&&pt.length===3){const t=ht(oa)-ht(pt),r=Math.max(.04,ht(pt)*.02);De=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const hr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Ge=Math.max(1,Math.ceil(pe.length/Ve)),ze=Math.min(ot,Ge),ia=(ze-1)*Ve,fr=pe.slice(ia,ia+Ve);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Mr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Gr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void Qa(t),className:`rounded border px-2 py-1 ${Dt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void Zt(!ke,_e),className:`rounded border px-2 py-1 ${ke?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void Zt(ke,!_e),className:`rounded border px-2 py-1 ${_e?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void tr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(kr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ar(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Br,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void er(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!0),className:`rounded border px-2 py-1 ${ta?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!1),className:`rounded border px-2 py-1 ${ta?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:tt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Lt,onChange:t=>Ut(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:at||!tt.trim(),onClick:()=>void rr(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Vt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Vt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",gt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:at,onClick:()=>void nr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:at,onClick:()=>void sr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.cpu_usage)==null?"—":`${Number(f.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.memory_usage)==null?"—":`${Number(f.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.temperature)==null?"—":`${Number(f.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>S?void dt():void Yt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${S?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(bt,{className:"mr-1 inline h-3.5 w-3.5"}),a(te?"cam.focus.starting":S?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(x?"cam.state.streaming":"cam.state.idle")]})]})]}),ye&&!x&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${S?"cursor-crosshair":""}`,onClick:Wa,children:[x&&ye&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),x?e.jsx("img",{ref:$,alt:"camera-preview",className:"h-full w-full object-contain",src:dr,onLoad:()=>{Z(null),Q(!1),lr()},onError:()=>{var r;if(fe(),!X.current)return;const t=(r=$.current)==null?void 0:r.src;(async()=>{if(!X.current||!t)return;if(await Pa()==="busy"){Z(a("cam.err.streamBusy")),Q(!0);return}Pe.current=window.setTimeout(()=>{X.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Fa,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"mr-1 inline h-3.5 w-3.5"}),a(T?"cam.btn.starting":"cam.btn.start")]})]}),x&&S&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Ra,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Ot/60).toString().padStart(2,"0")}:${(Ot%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Mt(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(Tt?"cam.hist.expand":"cam.hist.collapse")})}),!Tt&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Bt,onChange:t=>Ta(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ke,onChange:t=>Ma(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Xe,onChange:t=>Ia(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Pt,onChange:t=>Ha(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Ee,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:Ye.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:Ye.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[Ye.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${S?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(bt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${S?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(S?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),S?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[S?e.jsx("button",{type:"button",onClick:()=>void dt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>void Yt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(bt,{className:"mr-1 inline h-4 w-4"}),a(te?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!S,onClick:qa,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!S||j==null,onClick:La,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),S?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:Oe==null?"—":Oe.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(De)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:ae==null?"—":`${ae.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ae==null?"—":`+${Math.max(0,Ae).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(hr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:na==null?"—":`${na.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:sa==null?"—":`${(sa*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(zr,{samples:Se,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Aa).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((xa=n==null?void 0:n.info)==null?void 0:xa.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((pa=n==null?void 0:n.info)==null?void 0:pa.actual_exposure_us)!=null?gt((i==null?void 0:i.actual_exposure_us)??((ha=n==null?void 0:n.info)==null?void 0:ha.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(fa=n==null?void 0:n.info)!=null&&fa.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:ie(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Te.current!=null?`${Math.max(0,Math.round((performance.now()-Te.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ba=n==null?void 0:n.info)==null?void 0:ba.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ga=n==null?void 0:n.info)==null?void 0:ga.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((va=n==null?void 0:n.info)==null?void 0:va.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(ya=n==null?void 0:n.info)!=null&&ya.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ja=n==null?void 0:n.info)==null?void 0:ja.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((wa=n==null?void 0:n.info)==null?void 0:wa.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.width)??"—"}x${((Sa=n==null?void 0:n.info)==null?void 0:Sa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(ka=n==null?void 0:n.info)!=null&&ka.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!ra,onClick:()=>void Ua(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${ra?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Hr,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!xr,onClick:()=>void Va(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Fa,{className:"inline h-4 w-4"})," ",a(wt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!pr,onClick:()=>void Ja(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Ra,{className:"inline h-4 w-4"})," ",Je?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Jt,onClick:()=>void ut(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),za&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),_&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx($r,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:_.filename})]}),e.jsx("button",{type:"button",onClick:()=>mt(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(_r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ba(_.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:_.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(_.modified).toLocaleString()})]}),_.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:gt(_.exposure_us)})]}),_.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:_.analogue_gain})]}),_.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:_.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[pe.length===0&&!Jt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),fr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Fe===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ba(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ir(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Er,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void or(t.name),className:`rounded border px-2 py-1 ${Fe===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Pr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(wr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ze<=1,onClick:()=>Re(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:ze,total:Ge})}),e.jsx("button",{type:"button",disabled:ze>=Ge,onClick:()=>Re(t=>Math.min(Ge,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:St,onChange:t=>{kt(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:_t,onChange:t=>{Ct(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Et,onChange:t=>{$t(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Dr.map(t=>e.jsx("button",{type:"button",onClick:()=>{Rt(t),ee(!0)},className:`rounded border px-2 py-1 ${Ft===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!ue,onClick:()=>void Ka(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Ir,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Ie&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(G,{label:a("cam.controls.exposure"),value:u.exposureSeconds,min:1e-4,max:.5,step:1e-4,unit:" s",disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,exposureSeconds:t}))}}),e.jsx(G,{label:a("cam.controls.gain"),value:u.gain,min:1,max:24,step:.1,disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.digitalGain"),value:u.digitalGain,min:1,max:8,step:.1,disabled:Ie||!ea,onChange:t=>{w(!0),N(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:u.noiseReductionMode,onChange:t=>{w(!0),N(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:mr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(G,{label:a("cam.controls.contrast"),value:u.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.brightness"),value:u.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),N(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.saturation"),value:u.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.sharpness"),value:u.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),!ea&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((_a=n==null?void 0:n.info)==null?void 0:_a.actual_digital_gain)??u.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ce,onClick:()=>void Xa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:u.autoExposure?"auto":"manual",onChange:t=>{N(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:u.colorMode,onChange:t=>{N(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:u.whiteBalanceMode,onChange:t=>{N(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:ur.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:u.aeFlickerMode,onChange:t=>{N(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.5,step:.01,value:u.autoExposureMaxSeconds,onChange:t=>{N(r=>({...r,autoExposureMaxSeconds:k(Number(t.target.value)||.5,.01,.5)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(G,{label:"R Gain",value:u.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(G,{label:"B Gain",value:u.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!xt&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void Ya(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(le||de)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[le&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:le}),de&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:de})]})]})}vr.createRoot(document.getElementById("root")).render(e.jsx(yr.StrictMode,{children:e.jsx(Nr,{children:e.jsx(Fr,{children:e.jsx(qr,{})})})})); + */const Or=P("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Ar="/api/dev/debug";function H(a){return`${Ar}${a}`}async function h(a,b={}){const g=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Cr(g,b)}const Dr=["640x360","1280x720","1600x900","1920x1020"],Gr=[0,90,180,270],Ve=12;function k(a,b,g){return Math.min(g,Math.max(b,a))}function F(a,b){return a==null||Number.isNaN(Number(a))?b:Number(a)}function ie(a,b){return F(a,b)/1e6}function vt(a){return Math.round(a*1e6)}function gt(a){return`${ie(a,0).toFixed(4)} s`}function $a(a){const{exposureSeconds:b,autoExposureMaxSeconds:g,...f}=a;return{...f,exposure:vt(b),autoExposureMaxUs:vt(g)}}function Ba(a){if(!a)return"0 B";const b=["B","KB","MB","GB"];let g=0,f=a;for(;f>=1024&&gq.hfd),f=Math.min(...g),n=Math.max(...g),ce=Math.max(.2,n-f),x=g.map((q,K)=>{const le=K/Math.max(1,g.length-1)*320,l=76-(q-f)/ce*60;return`${le.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:x,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((z=x.split(" ").at(-1))==null?void 0:z.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Pa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const b=await a.json(),g=Number(b.max_clients??0),f=Number(b.active_clients??0);return g>0&&f>=g?"busy":"ok"}catch{return"fail"}}function G({label:a,value:b,min:g,max:f,step:n,onChange:ce,disabled:x=!1,unit:z=""}){const q=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${x?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(q),z]})]}),e.jsx("input",{type:"range",min:g,max:f,step:n,value:b,disabled:x,onChange:K=>ce(Number(K.target.value)),className:"w-full accent-primary"})]})}function qr(){var ca,la,da,ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a;const{t:a,locale:b,setLocale:g}=jr(),{info:f}=Sr(),[n,ce]=o.useState(null),[x,z]=o.useState(!1),[q,K]=o.useState(()=>Date.now()),[le,l]=o.useState(null),[ye,Z]=o.useState(null),[yt,Q]=o.useState(!1),[de,v]=o.useState(null),[T,je]=o.useState(!1),[Je,jt]=o.useState(!1),[wt,Nt]=o.useState(!1),[St,kt]=o.useState("8"),[_t,Ct]=o.useState("8"),[Ft,Rt]=o.useState("1280x720"),[Et,$t]=o.useState("supersample"),[ue,ee]=o.useState(!1),[Bt,Ta]=o.useState(!0),[Ke,Ma]=o.useState(!0),[Xe,Ia]=o.useState(!1),[Pt,Ha]=o.useState(!1),[Tt,Mt]=o.useState(!1),[Ye,Oa]=o.useState({mean:0,std:0,over:0}),[Aa,we]=o.useState(0),[i,Ze]=o.useState(null),[S,Qe]=o.useState(!1),[te,Ne]=o.useState(!1),[s,It]=o.useState(null),[Se,me]=o.useState([]),[ae,xe]=o.useState(null),[j,et]=o.useState(null),[Ht,O]=o.useState(null),[Ot,At]=o.useState(0),[Dt,Gt]=o.useState(180),[ke,zt]=o.useState(!1),[_e,qt]=o.useState(!1),[u,N]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ce,w]=o.useState(!1),[tt,Wt]=o.useState(""),[Lt,Ut]=o.useState(""),[Vt,Da]=o.useState([]),[at,W]=o.useState(!1),[pe,Ga]=o.useState([]),[Jt,Kt]=o.useState(!1),[_,rt]=o.useState(null),[za,nt]=o.useState(!1),[Fe,st]=o.useState(null),[ot,Re]=o.useState(1),$=o.useRef(null),Ee=o.useRef(null),he=o.useRef(null),$e=o.useRef(null),Be=o.useRef(null),L=o.useRef(null),Pe=o.useRef(null),it=o.useRef(!1),re=o.useRef(!1),X=o.useRef(!1),Te=o.useRef(null),U=o.useRef([]),ne=o.useRef(0),ct=o.useRef(null),Xt=()=>{U.current=[],ne.current=0,Te.current=null,we(0)},R=async()=>{try{const t=await h("/api/debug/camera/status",{cache:"no-store"});ce(t),t.streaming||(z(!1),X.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},fe=()=>{Pe.current!=null&&(window.clearTimeout(Pe.current),Pe.current=null)},lt=async()=>{if(T)return!1;je(!0),l(null),Z(null),Q(!1);try{fe(),n!=null&&n.streaming||await h("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Pa();return r==="busy"?(Z(a("cam.err.streamBusy")),Q(!0),!1):r==="fail"?(Z(a("cam.err.streamProbeFailed")),Q(!1),!1):(z(!0),X.current=!0,v(a("cam.notice.previewStart")),Xt(),Te.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{je(!1)}},Yt=async()=>{if(!te){Ne(!0),O(null);try{if(!(x||await lt()))return;const r=await h(H("/camera/focus/session/start"),{method:"POST"});re.current=r.active,N(c=>({...c,autoExposure:!1,exposureSeconds:ie(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),It(null),me([]),xe(null),et(null),Mt(!0),Qe(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},dt=async()=>{if(!te){Ne(!0),Qe(!1),O(null);try{if(re.current){const t=await h(H("/camera/focus/session/stop"),{method:"POST"});re.current=!1,t.restored_settings&&(N(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:ie(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){O(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},qa=()=>{me([]),xe(null),O(null)},Wa=t=>{if(!S||!$.current||t.target.closest("button, label, input"))return;const r=$.current,c=t.currentTarget.getBoundingClientRect(),p=r.naturalWidth||(s==null?void 0:s.frame.width)||0,d=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(p<=0||d<=0)return;const m=Math.min(c.width/p,c.height/d),C=p*m,y=d*m,E=(c.width-C)/2,B=(c.height-y)/2,D=(t.clientX-c.left-E)/C,se=(t.clientY-c.top-B)/y;D<0||D>1||se<0||se>1||(et({x:D,y:se}),me([]),xe(null),O(null))},La=()=>{et(null),me([]),xe(null),O(null)},Ua=async()=>{if(!T){je(!0),l(null);try{fe(),z(!1),X.current=!1,Z(null),Q(!1),Xt(),$.current&&($.current.onload=null,$.current.onerror=null,$.current.src="",$.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{je(!1)}}},Va=async()=>{if(!(!x&&!(n!=null&&n.streaming))){Nt(!0),l(null);try{const t=await h("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Nt(!1)}}},Ja=async()=>{if(!Je){jt(!0),l(null);try{if(n!=null&&n.recording)await h("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await h("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{jt(!1)}}},Ka=async()=>{l(null);try{const t=k(parseInt(St,10)||5,1,60);await h(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=k(parseInt(_t,10)||8,1,30);await h(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,p]=Ft.split("x").map(d=>parseInt(d,10));c&&p&&await h(`/api/debug/camera/size?width=${c}&height=${p}`,{method:"POST"}),await h(`/api/debug/camera/sampling?mode=${encodeURIComponent(Et)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),ee(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Xa=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ya=async()=>{l(null);try{await h("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify($a(u))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Za=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;N({exposureSeconds:k(ie(t.exposure_us,5e3),1e-4,.6),gain:k(F(t.analogue_gain,1),1,24),digitalGain:k(F(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:k(F(t.contrast,1),0,2),brightness:k(F(t.brightness,0),-1,1),saturation:k(F(t.saturation,1),0,2),sharpness:k(F(t.sharpness,1),0,2),noiseReduction:k(Math.round(F(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:k(ie(t.auto_exposure_max_us,6e5),.01,.6),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:k(F(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:k(F(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(kt(String(Math.round(F(t.fps,8)))),Rt(`${Math.round(F(t.width,1280))}x${Math.round(F(t.height,720))}`),$t(String(t.sampling_mode??"supersample")),ee(!1)),Gt(k(Math.round(F(t.rotation,180)),0,270)),zt(!!t.flip_horizontal),qt(!!t.flip_vertical),w(!1)},Zt=async(t,r)=>{l(null);try{await h("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),zt(t),qt(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},Qa=async t=>{l(null);try{await h(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Gt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},Qt=async t=>{l(null);try{await h(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},er=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await h("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},tr=async()=>{l(null);try{await h("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},ar=async()=>{l(null);try{await h("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Me=async()=>{W(!0);try{const t=await h("/api/debug/camera/presets",{cache:"no-store"});Da(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{W(!1)}},rr=async()=>{const t=tt.trim();if(t){W(!0),l(null);try{await h("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Lt.trim(),exposure_us:vt(u.exposureSeconds),analogue_gain:u.gain,digital_gain:u.digitalGain,auto_exposure:u.autoExposure,contrast:u.contrast,brightness:u.brightness,saturation:u.saturation,sharpness:u.sharpness,noise_reduction:u.noiseReduction,white_balance_mode:u.whiteBalanceMode,white_balance_gain_r:u.whiteBalanceGainR,white_balance_gain_b:u.whiteBalanceGainB,rotation:Dt,flip_horizontal:ke,flip_vertical:_e,color_mode:u.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Ut(""),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},nr=async t=>{W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}},sr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){W(!0),l(null);try{await h(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Me()}catch(r){l(r instanceof Error?r.message:String(r))}finally{W(!1)}}},ut=async()=>{Kt(!0);try{const t=await h("/api/debug/files",{cache:"no-store"});Ga(t.files??[]),Re(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Kt(!1)}},mt=()=>{st(null),rt(null),nt(!1)},or=async t=>{if(Fe===t){mt();return}st(t),rt(null),nt(!0),l(null);try{const r=await h(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});rt(r)}catch(r){l(r instanceof Error?r.message:String(r)),st(null)}finally{nt(!1)}},ir=t=>{const r=(p,d)=>{const m=document.createElement("a");m.href=d,m.download=p,document.body.appendChild(m),m.click(),document.body.removeChild(m)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const d=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(d)}`)).ok)return;r(d,`${H("/files")}/${encodeURIComponent(d)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:d}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},cr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await h(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Fe===t||(_==null?void 0:_.filename)===t)&&mt(),await ut()}catch(r){l(r instanceof Error?r.message:String(r))}}},lr=()=>{if(!Bt||!$.current||!Ee.current)return;const t=$.current;if(!t.naturalWidth||!t.naturalHeight||(he.current||(he.current=document.createElement("canvas"),$e.current=he.current.getContext("2d",{willReadFrequently:!0})),Be.current||(Be.current=Ee.current.getContext("2d")),!$e.current||!Be.current))return;const c=Math.min(1,320/t.naturalWidth),p=Math.max(1,Math.round(t.naturalWidth*c)),d=Math.max(1,Math.round(t.naturalHeight*c));he.current.width=p,he.current.height=d,$e.current.drawImage(t,0,0,p,d);const m=$e.current.getImageData(0,0,p,d).data,C=new Array(256).fill(0),y=new Array(256).fill(0),E=new Array(256).fill(0),B=new Array(256).fill(0);let D=0,se=0,Ca=0;const oe=p*d;for(let I=0;I=250&&(Ca+=1)}const ft=oe?D/oe:0,br=oe?se/oe-ft*ft:0;Oa({mean:ft,std:Math.sqrt(Math.max(0,br)),over:oe?Ca/oe*100:0});const qe=Ee.current,M=Be.current,We=window.devicePixelRatio||1,be=Math.max(1,qe.clientWidth||240),ge=Math.max(1,qe.clientHeight||120);qe.width=Math.floor(be*We),qe.height=Math.floor(ge*We),M.setTransform(We,0,0,We,0,0),M.clearRect(0,0,be,ge);const gr=Math.max(1,...Ke?[Math.max(...C),Math.max(...y),Math.max(...E)]:[0],...Xe?[Math.max(...B)]:[0]),Le=(I,Ue)=>{M.beginPath(),M.strokeStyle=Ue,M.lineWidth=1.2;for(let V=0;V<256;V+=1){const ve=V/255*be,J=ge-I[V]/gr*ge;V===0?M.moveTo(ve,J):M.lineTo(ve,J)}M.stroke()};if(Ke&&(Le(C,"rgba(255,80,80,0.85)"),Le(y,"rgba(80,255,80,0.85)"),Le(E,"rgba(80,160,255,0.85)")),Xe&&Le(B,"rgba(255,255,255,0.95)"),Pt){const I=.9803921568627451*be;M.fillStyle="rgba(255,100,100,0.12)",M.fillRect(I,0,be-I,ge)}};o.useEffect(()=>{R(),Me(),ut();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ce||Za(n.info,{syncRuntime:!ue})},[n==null?void 0:n.info,Ce,ue]),o.useEffect(()=>{if(!(n!=null&&n.recording)){L.current&&(window.clearInterval(L.current),L.current=null),At(0);return}if(L.current)return;const t=Date.now();return L.current=window.setInterval(()=>{At(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{L.current&&(window.clearInterval(L.current),L.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{X.current=x,x||(fe(),Qe(!1),re.current&&dt())},[x]),o.useEffect(()=>()=>{re.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),re.current=!1)},[]),o.useEffect(()=>{if(!S||!x)return;let t=!1;const r=async()=>{var p,d;if(!(it.current||document.hidden)){it.current=!0;try{const m=new URLSearchParams;j&&(m.set("target_x",j.x.toFixed(6)),m.set("target_y",j.y.toFixed(6)));const C=m.size>0?`?${m.toString()}`:"",y=await h(`${H("/camera/focus/metrics")}${C}`,{cache:"no-store"});if(t)return;It(y),O(null);const E=j?(p=y.selected_star)==null?void 0:p.hfd_px:(d=y.aggregate)==null?void 0:d.median_hfd_px;typeof E=="number"&&Number.isFinite(E)&&(me(B=>{var D;return((D=B.at(-1))==null?void 0:D.frameId)===y.frame_id?B:[...B,{frameId:y.frame_id,hfd:E}].slice(-60)}),xe(B=>B==null?E:Math.min(B,E)))}catch(m){t||O(m instanceof Error?m.message:String(m))}finally{it.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[S,x,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{x&&(U.current=[],ne.current=0)},[q,x]),o.useEffect(()=>{if(!x){U.current=[],ne.current=0,we(0);return}if(!ct.current){const d=document.createElement("canvas");d.width=32,d.height=32,ct.current=d}const r=ct.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const p=()=>{const d=$.current;if(d!=null&&d.complete&&d.naturalWidth>0)try{r.drawImage(d,0,0,32,32);const m=r.getImageData(0,0,32,32).data;let C=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[x]),o.useEffect(()=>{if(!x){we(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,p=U.current;for(;p.length&&p[0]window.clearInterval(t)},[x]),o.useEffect(()=>{if(!x){Ze(null);return}let t=!1;const r=async()=>{try{const p=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!p.ok||t)return;const d=await p.json();if(!t){Ze(d);const m=Number(d.preview_target_fps??d.target_preview_fps??0);Number.isFinite(m)&&m>0&&!ue&&Ct(String(Math.round(m)))}}catch{t||Ze(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[x,ue]),o.useEffect(()=>{if(!de)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[de]),o.useEffect(()=>()=>fe(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(pe.length/Ve));ot>t&&Re(t)},[pe.length,ot]);const dr=x?`${H("/camera/stream")}?t=${q}`:"",Ie=u.autoExposure,xt=u.whiteBalanceMode==="manual",Y=((ca=n==null?void 0:n.info)==null?void 0:ca.capabilities)??{},ur=Array.isArray(Y.awb_modes)&&Y.awb_modes.length>0?Y.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],mr=Array.isArray(Y.noise_reduction_modes)&&Y.noise_reduction_modes.length>0?Y.noise_reduction_modes:["off","fast","high_quality"],ea=Y.manual_digital_gain!==!1,ta=!!((la=n==null?void 0:n.info)!=null&&la.night_mode),aa=x,He=!T&&!x&&!(n!=null&&n.recording),ra=!T&&x,xr=!T&&!wt&&aa,pr=!T&&!Je&&aa,A=j?s==null?void 0:s.selected_star:null,Oe=j?(A==null?void 0:A.hfd_px)??null:((da=s==null?void 0:s.aggregate)==null?void 0:da.median_hfd_px)??null,na=j?(A==null?void 0:A.fwhm_px)??null:((ua=s==null?void 0:s.aggregate)==null?void 0:ua.median_fwhm_px)??null,sa=j?(A==null?void 0:A.concentration)??null:((ma=s==null?void 0:s.aggregate)==null?void 0:ma.median_concentration)??null,Ae=Oe!=null&&ae!=null&&ae>0?(Oe-ae)/ae*100:null,oa=Se.slice(-3).map(t=>t.hfd),pt=Se.slice(-6,-3).map(t=>t.hfd),ht=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let De="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")De="cam.focus.guidance.noStars";else if(Se.length>=5&&Ae!=null&&Ae<=3)De="cam.focus.guidance.best";else if(oa.length===3&&pt.length===3){const t=ht(oa)-ht(pt),r=Math.max(.04,ht(pt)*.02);De=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const hr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Ge=Math.max(1,Math.ceil(pe.length/Ve)),ze=Math.min(ot,Ge),ia=(ze-1)*Ve,fr=pe.slice(ia,ia+Ve);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>g("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Mr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Gr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void Qa(t),className:`rounded border px-2 py-1 ${Dt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void Zt(!ke,_e),className:`rounded border px-2 py-1 ${ke?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void Zt(ke,!_e),className:`rounded border px-2 py-1 ${_e?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void tr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(kr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void ar(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Br,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void er(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!0),className:`rounded border px-2 py-1 ${ta?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void Qt(!1),className:`rounded border px-2 py-1 ${ta?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:tt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Lt,onChange:t=>Ut(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:at||!tt.trim(),onClick:()=>void rr(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Vt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Vt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",gt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:at,onClick:()=>void nr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:at,onClick:()=>void sr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.cpu_usage)==null?"—":`${Number(f.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.memory_usage)==null?"—":`${Number(f.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(f==null?void 0:f.temperature)==null?"—":`${Number(f.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>S?void dt():void Yt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${S?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(bt,{className:"mr-1 inline h-3.5 w-3.5"}),a(te?"cam.focus.starting":S?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(x?"cam.state.streaming":"cam.state.idle")]})]})]}),ye&&!x&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${S?"cursor-crosshair":""}`,onClick:Wa,children:[x&&ye&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:ye}),yt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),x?e.jsx("img",{ref:$,alt:"camera-preview",className:"h-full w-full object-contain",src:dr,onLoad:()=>{Z(null),Q(!1),lr()},onError:()=>{var r;if(fe(),!X.current)return;const t=(r=$.current)==null?void 0:r.src;(async()=>{if(!X.current||!t)return;if(await Pa()==="busy"){Z(a("cam.err.streamBusy")),Q(!0);return}Pe.current=window.setTimeout(()=>{X.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Fa,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"mr-1 inline h-3.5 w-3.5"}),a(T?"cam.btn.starting":"cam.btn.start")]})]}),x&&S&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Ra,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Ot/60).toString().padStart(2,"0")}:${(Ot%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>Mt(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(Tt?"cam.hist.expand":"cam.hist.collapse")})}),!Tt&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Bt,onChange:t=>Ta(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ke,onChange:t=>Ma(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Xe,onChange:t=>Ia(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Pt,onChange:t=>Ha(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Ee,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:Ye.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:Ye.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[Ye.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${S?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(bt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${S?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(S?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),S?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[S?e.jsx("button",{type:"button",onClick:()=>void dt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:te||T||!!(n!=null&&n.recording),onClick:()=>void Yt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(bt,{className:"mr-1 inline h-4 w-4"}),a(te?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!S,onClick:qa,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!S||j==null,onClick:La,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),S?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:Oe==null?"—":Oe.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(De)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:ae==null?"—":`${ae.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ae==null?"—":`+${Math.max(0,Ae).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(hr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:na==null?"—":`${na.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:sa==null?"—":`${(sa*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(zr,{samples:Se,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Aa).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((xa=n==null?void 0:n.info)==null?void 0:xa.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((pa=n==null?void 0:n.info)==null?void 0:pa.actual_exposure_us)!=null?gt((i==null?void 0:i.actual_exposure_us)??((ha=n==null?void 0:n.info)==null?void 0:ha.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(fa=n==null?void 0:n.info)!=null&&fa.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:ie(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Te.current!=null?`${Math.max(0,Math.round((performance.now()-Te.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ba=n==null?void 0:n.info)==null?void 0:ba.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ga=n==null?void 0:n.info)==null?void 0:ga.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((va=n==null?void 0:n.info)==null?void 0:va.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(ya=n==null?void 0:n.info)!=null&&ya.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ja=n==null?void 0:n.info)==null?void 0:ja.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((wa=n==null?void 0:n.info)==null?void 0:wa.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.width)??"—"}x${((Sa=n==null?void 0:n.info)==null?void 0:Sa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(ka=n==null?void 0:n.info)!=null&&ka.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void lt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ea,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!ra,onClick:()=>void Ua(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${ra?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Hr,{className:"inline h-4 w-4"})," ",a(T?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!xr,onClick:()=>void Va(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Fa,{className:"inline h-4 w-4"})," ",a(wt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!pr,onClick:()=>void Ja(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Ra,{className:"inline h-4 w-4"})," ",Je?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Jt,onClick:()=>void ut(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),za&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),_&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx($r,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:_.filename})]}),e.jsx("button",{type:"button",onClick:()=>mt(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(_r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ba(_.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:_.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(_.modified).toLocaleString()})]}),_.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:gt(_.exposure_us)})]}),_.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:_.analogue_gain})]}),_.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:_.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[pe.length===0&&!Jt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),fr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Fe===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ba(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ir(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Er,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void or(t.name),className:`rounded border px-2 py-1 ${Fe===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Pr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(wr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:ze<=1,onClick:()=>Re(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:ze,total:Ge})}),e.jsx("button",{type:"button",disabled:ze>=Ge,onClick:()=>Re(t=>Math.min(Ge,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:St,onChange:t=>{kt(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:_t,onChange:t=>{Ct(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Et,onChange:t=>{$t(t.target.value),ee(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Dr.map(t=>e.jsx("button",{type:"button",onClick:()=>{Rt(t),ee(!0)},className:`rounded border px-2 py-1 ${Ft===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!ue,onClick:()=>void Ka(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Ir,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ce&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Ie&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(G,{label:a("cam.controls.exposure"),value:u.exposureSeconds,min:1e-4,max:.6,step:1e-4,unit:" s",disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,exposureSeconds:t}))}}),e.jsx(G,{label:a("cam.controls.gain"),value:u.gain,min:1,max:24,step:.1,disabled:Ie,onChange:t=>{w(!0),N(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.digitalGain"),value:u.digitalGain,min:1,max:8,step:.1,disabled:Ie||!ea,onChange:t=>{w(!0),N(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:u.noiseReductionMode,onChange:t=>{w(!0),N(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:mr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(G,{label:a("cam.controls.contrast"),value:u.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.brightness"),value:u.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),N(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.saturation"),value:u.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(G,{label:a("cam.controls.sharpness"),value:u.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),N(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),!ea&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((_a=n==null?void 0:n.info)==null?void 0:_a.actual_digital_gain)??u.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ce,onClick:()=>void Xa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:u.autoExposure?"auto":"manual",onChange:t=>{N(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:u.colorMode,onChange:t=>{N(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:u.whiteBalanceMode,onChange:t=>{N(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:ur.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:u.aeFlickerMode,onChange:t=>{N(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.6,step:.01,value:u.autoExposureMaxSeconds,onChange:t=>{N(r=>({...r,autoExposureMaxSeconds:k(Number(t.target.value)||.6,.01,.6)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(G,{label:"R Gain",value:u.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(G,{label:"B Gain",value:u.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!xt,onChange:t=>{N(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!xt&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void Ya(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(le||de)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[le&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:le}),de&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:de})]})]})}vr.createRoot(document.getElementById("root")).render(e.jsx(yr.StrictMode,{children:e.jsx(Nr,{children:e.jsx(Fr,{children:e.jsx(qr,{})})})})); diff --git a/web/static/analysis-lab/camera.html b/web/static/analysis-lab/camera.html index ea814ec..1f65ac6 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,7 +4,7 @@ OGScope 相机调试控制台 - + From 9d292ff5123c79eb22fb04064f1fa84c493f8aea 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: Thu, 27 Aug 2026 16:06:41 +0800 Subject: [PATCH 14/24] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=9B=9D=E5=85=89=E9=97=AD=E7=8E=AF=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=99=BD=E5=A4=A9=E8=BF=87=E6=9B=9D=20/=20restore=20A?= =?UTF-8?q?E=20loop=20and=20fix=20daylight=20overexposure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ogscope/platform/hardware/camera.py | 76 ++++++++++++++++++++--------- tests/unit/test_camera_flip.py | 57 ++++++++++++++++++++-- 2 files changed, 106 insertions(+), 27 deletions(-) diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 4fd9910..12e28ac 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -63,7 +63,7 @@ class IMX327MIPICamera(CameraInterface): SENSOR_MAX_HEIGHT = 1020 PREVIEW_BUFFER_COUNT = 2 AE_SUPERVISOR_INTERVAL_S = 1.0 - AE_SUPERVISOR_STEP_EV = 0.25 + AE_SUPERVISOR_STEP_EV = 0.5 MANUAL_CONTROL_RANGE_DEFAULTS = { "ExposureTime": {"min": 1000, "max": 100000, "default": 10000, "step": 1000}, "AnalogueGain": {"min": 1.0, "max": 16.0, "default": 1.0, "step": 0.1}, @@ -135,7 +135,11 @@ def __init__(self, config: dict[str, Any]): self.ae_polar_preset = bool(config.get("ae_polar_preset", True)) self.ae_exposure_value = float(config.get("ae_exposure_value", 1.0)) self.ae_aggressive_enabled = bool(config.get("ae_aggressive_enabled", True)) - self._ae_effective_exposure_value = self.ae_exposure_value + # Start every AE session from neutral. Dark-scene supervision may then + # raise EV, while daylight never inherits a night-sky bias at startup. + # 每次 AE 会话都从中性补偿开始;暗场监督随后按需抬升 EV,白天启动时不会 + # 继承夜空增益偏置。 + self._ae_effective_exposure_value = 0.0 self._ae_last_adjust_at = 0.0 logger.info( @@ -566,8 +570,17 @@ def _update_aggressive_auto_exposure(self, stats: dict[str, Any]) -> None: if abs(delta) < 0.1: self._ae_last_adjust_at = now return - step = min(abs(delta), self.AE_SUPERVISOR_STEP_EV) - next_ev = current + (step if delta > 0 else -step) + if desired <= 0.0 and ( + float(stats.get("p50") or 0.0) >= 180.0 + or float(stats.get("saturated_fraction") or 0.0) >= 0.20 + ): + # Broad clipping needs an immediate daylight escape; a slow EV ramp + # leaves the preview unusable for several frames. + # 大面积过曝时立即回到日间中性值;逐档回落会让预览连续多帧不可用。 + next_ev = 0.0 + else: + step = min(abs(delta), self.AE_SUPERVISOR_STEP_EV) + next_ev = current + (step if delta > 0 else -step) controls = self._camera_controls() try: if "ExposureValue" in controls: @@ -640,11 +653,13 @@ def _apply_polar_auto_exposure_controls(self) -> None: if hasattr(pcc, "AeExposureModeEnum"): updates["AeExposureMode"] = pcc.AeExposureModeEnum.Long ev = float(self._ae_effective_exposure_value) - if abs(ev) > 1e-6: - if "ExposureValue" in cc: - updates["ExposureValue"] = ev - elif "Brightness" in cc: - updates["Brightness"] = max(-1.0, min(1.0, ev * 0.2)) + # Explicitly write the neutral value too. Omitting zero can leave a + # previous dark-scene bias active after returning to daylight. + # 中性值也必须显式写入;省略零值会让此前的暗场补偿残留到白天。 + if "ExposureValue" in cc: + updates["ExposureValue"] = ev + elif "Brightness" in cc: + updates["Brightness"] = max(-1.0, min(1.0, ev * 0.2)) try: self._apply_frame_duration_controls() @@ -724,17 +739,30 @@ def initialize(self) -> bool: # 设置相机控制参数 / Set camera control parameters # 构建控制参数,兼容部分固件未提供 DigitalGain 的情况 / Build control parameters, compatible with some firmwares that do not provide DigitalGain - controls = { - "ExposureTime": self.exposure_us, - "AnalogueGain": self.analogue_gain, - "AeEnable": self.auto_exposure, - } + controls = {"AeEnable": self.auto_exposure} + if not self.auto_exposure: + # Manual exposure/gain must not be queued together with AE. + # Some IMX327/libcamera combinations keep those seed values + # latched, which can leave daylight fixed at 10 ms and white. + # 自动曝光启动时不能同时排队手动曝光/增益;部分 IMX327/libcamera + # 组合会锁住这些初值,导致白天固定在 10 ms 并整幅过曝。 + controls.update( + { + "ExposureTime": self.exposure_us, + "AnalogueGain": self.analogue_gain, + } + ) controls.update(self._white_balance_controls()) - try: - self.camera.set_controls({**controls, "DigitalGain": self.digital_gain}) - except Exception: - # DigitalGain 不被支持时,退化为不设置该项 / When DigitalGain is not supported, it will degenerate to not setting this item. + if self.auto_exposure: self.camera.set_controls(controls) + else: + try: + self.camera.set_controls( + {**controls, "DigitalGain": self.digital_gain} + ) + except Exception: + # DigitalGain 不被支持时,退化为不设置该项 / When DigitalGain is unsupported, omit it. + self.camera.set_controls(controls) if self.auto_exposure: self._apply_polar_auto_exposure_controls() @@ -766,11 +794,11 @@ def start_capture(self) -> bool: # 设置帧周期(优先 FrameDurationLimits) / Set frame period (prefer FrameDurationLimits). self._apply_frame_duration_controls() - # 重新配置后重放曝光控制,避免状态漂移到驱动默认值 / Replay exposure control after reconfiguration to avoid state drift to driver defaults + # 手动控制可在启动前排队;AE 必须在流启动后再次启用,确保 IMX327/libcamera + # 真正进入闭环而不是停留在初始化曝光。 / Manual controls may be queued + # before start; re-enable AE after streaming so the driver enters its loop. try: - if self.auto_exposure: - self._apply_polar_auto_exposure_controls() - else: + if not self.auto_exposure: controls = { "AeEnable": False, "ExposureTime": self.exposure_us, @@ -790,6 +818,8 @@ def start_capture(self) -> bool: self.camera.start() self.is_capturing = True + if self.auto_exposure: + self._apply_polar_auto_exposure_controls() logger.info("相机开始捕获") return True except Exception as e: @@ -1087,7 +1117,7 @@ def set_auto_exposure(self, enabled: bool) -> bool: try: self.auto_exposure = enabled if enabled: - self._ae_effective_exposure_value = self.ae_exposure_value + self._ae_effective_exposure_value = 0.0 self._ae_last_adjust_at = 0.0 self._apply_polar_auto_exposure_controls() else: diff --git a/tests/unit/test_camera_flip.py b/tests/unit/test_camera_flip.py index c0722e3..27b2147 100644 --- a/tests/unit/test_camera_flip.py +++ b/tests/unit/test_camera_flip.py @@ -63,6 +63,7 @@ class _FakePicamera2: def __init__(self) -> None: self.controls_log: list[dict] = [] self.camera_controls = {} + self.started = False def create_video_configuration(self, **kwargs): return kwargs @@ -73,6 +74,9 @@ def configure(self, _config) -> None: def set_controls(self, controls: dict) -> None: self.controls_log.append(dict(controls)) + def start(self) -> None: + self.started = True + class _FakeCompletedRequest: """最小完成请求替身 / Minimal completed-request test double.""" @@ -128,6 +132,50 @@ def test_initialize_auto_white_balance_really_enables_awb(monkeypatch) -> None: assert any(item.get("AwbEnable") is True for item in fake.controls_log) +@pytest.mark.unit +def test_initialize_auto_exposure_does_not_seed_manual_controls(monkeypatch) -> None: + """自动模式不得锁入手动初值 / Auto mode must not latch manual seed controls.""" + fake = _FakePicamera2() + monkeypatch.setitem( + sys.modules, + "picamera2", + types.SimpleNamespace(Picamera2=lambda: fake), + ) + cam = IMX327MIPICamera( + _minimal_config( + auto_exposure=True, + exposure_us=10_000, + analogue_gain=2.0, + digital_gain=1.5, + ) + ) + + assert cam.initialize() is True + auto_writes = [item for item in fake.controls_log if item.get("AeEnable") is True] + assert auto_writes + assert all("ExposureTime" not in item for item in auto_writes) + assert all("AnalogueGain" not in item for item in auto_writes) + assert all("DigitalGain" not in item for item in auto_writes) + + +@pytest.mark.unit +def test_start_capture_reapplies_auto_exposure_after_stream_start(monkeypatch) -> None: + """相机开始出帧后再次启用 AE / Re-enable AE after streaming starts.""" + fake = _FakePicamera2() + cam = IMX327MIPICamera(_minimal_config(auto_exposure=True)) + cam.camera = fake + cam.is_initialized = True + applied_after_start: list[bool] = [] + monkeypatch.setattr( + cam, + "_apply_polar_auto_exposure_controls", + lambda: applied_after_start.append(fake.started), + ) + + assert cam.start_capture() is True + assert applied_after_start == [True] + + @pytest.mark.unit def test_initialize_manual_white_balance_sets_colour_gains(monkeypatch) -> None: fake = _FakePicamera2() @@ -247,8 +295,8 @@ def test_aggressive_ae_ignores_small_clipped_highlight() -> None: {"p50": 20.0, "p90": 35.0, "p99": 80.0, "saturated_fraction": 0.01} ) - assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(1.25) - assert cam._ae_effective_exposure_value == pytest.approx(1.25) + assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(0.5) + assert cam._ae_effective_exposure_value == pytest.approx(0.5) @pytest.mark.unit @@ -257,13 +305,14 @@ def test_aggressive_ae_returns_toward_neutral_in_bright_scene() -> None: fake.camera_controls = {"ExposureValue": (-2.0, 2.0, 0.0)} cam = IMX327MIPICamera(_minimal_config(auto_exposure=True, ae_exposure_value=1.0)) cam.camera = fake + cam._ae_effective_exposure_value = 1.5 cam._update_aggressive_auto_exposure( {"p50": 220.0, "p90": 250.0, "p99": 255.0, "saturated_fraction": 0.25} ) - assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(0.75) - assert cam._ae_effective_exposure_value == pytest.approx(0.75) + assert fake.controls_log[-1]["ExposureValue"] == pytest.approx(0.0) + assert cam._ae_effective_exposure_value == pytest.approx(0.0) @pytest.mark.unit From 65e5914cbcdaad52ccbb06e0d4268bfbb15da7e6 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: Thu, 27 Aug 2026 17:30:29 +0800 Subject: [PATCH 15/24] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E6=9B=9D?= =?UTF-8?q?=E5=85=89=E4=B8=8A=E9=99=90=E4=B8=BA0.6=E7=A7=92=20/=20align=20?= =?UTF-8?q?exposure=20ceiling=20to=200.6=20seconds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DEBUG_CONSOLE.md | 2 +- docs/DEBUG_CONSOLE_EN.md | 2 +- web/spa/src/apps/camera/CameraConsoleApp.tsx | 4 ++-- .../assets/{camera-D60LudQ0.js => camera-q9IdHzgf.js} | 2 +- web/static/analysis-lab/camera.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename web/static/analysis-lab/assets/{camera-D60LudQ0.js => camera-q9IdHzgf.js} (99%) diff --git a/docs/DEBUG_CONSOLE.md b/docs/DEBUG_CONSOLE.md index b7bf5ad..43c6427 100644 --- a/docs/DEBUG_CONSOLE.md +++ b/docs/DEBUG_CONSOLE.md @@ -27,7 +27,7 @@ OGScope 调试控制台是一个专为开发者设计的相机调试工具,提 - **参数记录**: 每次拍摄自动生成参数记录文件 ### ⚙️ 参数设置 -- **曝光时间**: 1ms - 100ms (微秒级调节) +- **曝光时间**: 0.1ms–0.6s(微秒级调节,与自动曝光上限一致) - **模拟增益**: 1x - 16x (0.1x步进) - **数字增益**: 1x - 4x (0.1x步进) - **白平衡**: `auto` / `manual` / `night`,手动模式可设置红/蓝增益 diff --git a/docs/DEBUG_CONSOLE_EN.md b/docs/DEBUG_CONSOLE_EN.md index 53a882e..52b01c1 100644 --- a/docs/DEBUG_CONSOLE_EN.md +++ b/docs/DEBUG_CONSOLE_EN.md @@ -27,7 +27,7 @@ The OGScope debug console is a developer-focused camera tool: live preview, capt - **Sidecar metadata**: `.txt` parameter file per capture ### Parameters -- **Exposure**: 1ms–100ms (fine steps) +- **Exposure**: 0.1ms–0.6s (fine steps, aligned with the auto-exposure ceiling) - **Analog gain**: 1x–16x (0.1x steps) - **Digital gain**: 1x–4x (0.1x steps) - **White balance**: `auto` / `manual` / `night`; manual mode exposes red/blue gains diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index 0694c88..142bc81 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -241,7 +241,7 @@ const RES_PRESETS = ["640x360", "1280x720", "1600x900", "1920x1020"] as const; const ROTATION_PRESETS = [0, 90, 180, 270] as const; const FILE_PAGE_SIZE = 12; const MANUAL_EXPOSURE_MIN_SECONDS = 0.0001; -const MANUAL_EXPOSURE_MAX_SECONDS = 0.5; +const MANUAL_EXPOSURE_MAX_SECONDS = 0.6; const MANUAL_EXPOSURE_STEP_SECONDS = 0.0001; function clamp(v: number, min: number, max: number): number { @@ -430,7 +430,7 @@ function ExposureControl({ disabled={disabled} aria-label={label} onChange={(e) => { - // 对数滑块兼顾短曝光精调与 0.5 秒长曝光 / Log scale preserves short-exposure precision up to 0.5 s. + // 对数滑块兼顾短曝光精调与 0.6 秒长曝光 / Log scale preserves short-exposure precision up to 0.6 s. const next = Number((10 ** Number(e.target.value)).toFixed(6)); onChange(clamp(next, MANUAL_EXPOSURE_MIN_SECONDS, MANUAL_EXPOSURE_MAX_SECONDS)); }} diff --git a/web/static/analysis-lab/assets/camera-D60LudQ0.js b/web/static/analysis-lab/assets/camera-q9IdHzgf.js similarity index 99% rename from web/static/analysis-lab/assets/camera-D60LudQ0.js rename to web/static/analysis-lab/assets/camera-q9IdHzgf.js index 59cdb80..5f9546c 100644 --- a/web/static/analysis-lab/assets/camera-D60LudQ0.js +++ b/web/static/analysis-lab/assets/camera-q9IdHzgf.js @@ -58,4 +58,4 @@ import{r as o,j as e,a as Sr,R as kr}from"./client-D1ZVDB-N.js";import{c as T,u * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Lr=T("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),qr="/api/dev/debug";function H(a){return`${qr}${a}`}async function b(a,g={}){const f=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Br(f,g)}const Ur=["640x360","1280x720","1600x900","1920x1020"],Wr=[0,90,180,270],Je=12,ce=1e-4,le=.5,Vr=1e-4;function N(a,g,f){return Math.min(f,Math.max(g,a))}function _(a,g){return a==null||Number.isNaN(Number(a))?g:Number(a)}function de(a,g){return _(a,g)/1e6}function wt(a){return Math.round(a*1e6)}function jt(a){return`${de(a,0).toFixed(4)} s`}function Ta(a){const{exposureSeconds:g,autoExposureMaxSeconds:f,...p}=a;return{...p,exposure:wt(g),autoExposureMaxUs:wt(f)}}function Ia(a){if(!a)return"0 B";const g=["B","KB","MB","GB"];let f=0,p=a;for(;p>=1024&&fE.hfd),p=Math.min(...f),n=Math.max(...f),J=Math.max(.2,n-p),m=f.map((E,K)=>{const ue=K/Math.max(1,f.length-1)*320,l=76-(E-p)/J*60;return`${ue.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:m,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((B=m.split(" ").at(-1))==null?void 0:B.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Aa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const g=await a.json(),f=Number(g.max_clients??0),p=Number(g.active_clients??0);return f>0&&p>=f?"busy":"ok"}catch{return"fail"}}function X({label:a,value:g,min:f,max:p,step:n,onChange:J,disabled:m=!1,unit:B=""}){const E=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${m?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[g.toFixed(E),B]})]}),e.jsx("input",{type:"range",min:f,max:p,step:n,value:g,disabled:m,onChange:K=>J(Number(K.target.value)),className:"w-full accent-primary"})]})}function Jr({label:a,value:g,disabled:f,onChange:p}){const n=N(g,ce,le),J=Math.log10(ce),m=Math.log10(le);return e.jsxs("div",{className:`block ${f?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between gap-2",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("input",{type:"number",min:ce,max:le,step:Vr,value:Number(n.toFixed(6)),disabled:f,"aria-label":a,onChange:B=>{const E=Number(B.target.value);Number.isFinite(E)&&p(N(E,ce,le))},className:"w-24 rounded border border-outline-variant/30 bg-surface-container-low px-1.5 py-0.5 text-right font-mono text-[11px]"}),e.jsx("span",{className:"font-mono text-[11px]",children:"s"})]})]}),e.jsx("input",{type:"range",min:J,max:m,step:.001,value:Math.log10(n),disabled:f,"aria-label":a,onChange:B=>{const E=Number((10**Number(B.target.value)).toFixed(6));p(N(E,ce,le))},className:"w-full accent-primary"})]})}function Kr(){var ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a,Ca,Fa,Ea,Ra;const{t:a,locale:g,setLocale:f}=_r(),{info:p}=Er(),[n,J]=o.useState(null),[m,B]=o.useState(!1),[E,K]=o.useState(()=>Date.now()),[ue,l]=o.useState(null),[we,Q]=o.useState(null),[Nt,ee]=o.useState(!1),[me,v]=o.useState(null),[I,Ne]=o.useState(!1),[Ke,St]=o.useState(!1),[kt,_t]=o.useState(!1),[Ye,Ct]=o.useState("8"),[Ft,Et]=o.useState("8"),[Rt,$t]=o.useState("1280x720"),[Bt,Pt]=o.useState("supersample"),[xe,te]=o.useState(!1),[Mt,Oa]=o.useState(!0),[Ze,Ha]=o.useState(!0),[Qe,Da]=o.useState(!1),[Tt,Ga]=o.useState(!1),[It,At]=o.useState(!1),[et,za]=o.useState({mean:0,std:0,over:0}),[La,Se]=o.useState(0),[i,tt]=o.useState(null),[k,at]=o.useState(!1),[ae,ke]=o.useState(!1),[s,Ot]=o.useState(null),[_e,pe]=o.useState([]),[re,he]=o.useState(null),[j,rt]=o.useState(null),[Ht,D]=o.useState(null),[Dt,Gt]=o.useState(0),[zt,Lt]=o.useState(180),[Ce,qt]=o.useState(!1),[Fe,Ut]=o.useState(!1),[d,S]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ee,w]=o.useState(!1),[nt,Wt]=o.useState(""),[Vt,Xt]=o.useState(""),[Jt,qa]=o.useState([]),[st,L]=o.useState(!1),[fe,Ua]=o.useState([]),[Kt,Yt]=o.useState(!1),[C,ot]=o.useState(null),[Wa,it]=o.useState(!1),[Re,ct]=o.useState(null),[lt,$e]=o.useState(1),P=o.useRef(null),Be=o.useRef(null),be=o.useRef(null),Pe=o.useRef(null),Me=o.useRef(null),q=o.useRef(null),Te=o.useRef(null),dt=o.useRef(!1),ne=o.useRef(!1),Y=o.useRef(!1),Ie=o.useRef(null),U=o.useRef([]),se=o.useRef(0),ut=o.useRef(null),Zt=()=>{U.current=[],se.current=0,Ie.current=null,Se(0)},R=async()=>{try{const t=await b("/api/debug/camera/status",{cache:"no-store"});J(t),t.streaming||(B(!1),Y.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},ge=()=>{Te.current!=null&&(window.clearTimeout(Te.current),Te.current=null)},mt=async()=>{if(I)return!1;Ne(!0),l(null),Q(null),ee(!1);try{ge(),n!=null&&n.streaming||await b("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Aa();return r==="busy"?(Q(a("cam.err.streamBusy")),ee(!0),!1):r==="fail"?(Q(a("cam.err.streamProbeFailed")),ee(!1),!1):(B(!0),Y.current=!0,v(a("cam.notice.previewStart")),Zt(),Ie.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{Ne(!1)}},Qt=async()=>{if(!ae){ke(!0),D(null);try{if(!(m||await mt()))return;const r=await b(H("/camera/focus/session/start"),{method:"POST"});ne.current=r.active,S(c=>({...c,autoExposure:!1,exposureSeconds:de(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),Ot(null),pe([]),he(null),rt(null),At(!0),at(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},xt=async()=>{if(!ae){ke(!0),at(!1),D(null);try{if(ne.current){const t=await b(H("/camera/focus/session/stop"),{method:"POST"});ne.current=!1,t.restored_settings&&(S(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:de(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},Va=()=>{pe([]),he(null),D(null)},Xa=t=>{if(!k||!P.current||t.target.closest("button, label, input"))return;const r=P.current,c=t.currentTarget.getBoundingClientRect(),h=r.naturalWidth||(s==null?void 0:s.frame.width)||0,u=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(h<=0||u<=0)return;const x=Math.min(c.width/h,c.height/u),F=h*x,y=u*x,$=(c.width-F)/2,M=(c.height-y)/2,z=(t.clientX-c.left-$)/F,oe=(t.clientY-c.top-M)/y;z<0||z>1||oe<0||oe>1||(rt({x:z,y:oe}),pe([]),he(null),D(null))},Ja=()=>{rt(null),pe([]),he(null),D(null)},Ka=async()=>{if(!I){Ne(!0),l(null);try{ge(),B(!1),Y.current=!1,Q(null),ee(!1),Zt(),P.current&&(P.current.onload=null,P.current.onerror=null,P.current.src="",P.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},Ya=async()=>{if(!(!m&&!(n!=null&&n.streaming))){_t(!0),l(null);try{const t=await b("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{_t(!1)}}},Za=async()=>{if(!Ke){St(!0),l(null);try{if(n!=null&&n.recording)await b("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await b("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{St(!1)}}},Qa=async()=>{l(null);try{const t=N(parseInt(Ye,10)||5,1,60);await b(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=N(parseInt(Ft,10)||8,1,30);await b(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,h]=Rt.split("x").map(u=>parseInt(u,10));c&&h&&await b(`/api/debug/camera/size?width=${c}&height=${h}`,{method:"POST"}),await b(`/api/debug/camera/sampling?mode=${encodeURIComponent(Bt)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),te(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},er=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},tr=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},ar=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;S({exposureSeconds:N(de(t.exposure_us,5e3),ce,le),gain:N(_(t.analogue_gain,1),1,24),digitalGain:N(_(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:N(_(t.contrast,1),0,2),brightness:N(_(t.brightness,0),-1,1),saturation:N(_(t.saturation,1),0,2),sharpness:N(_(t.sharpness,1),0,2),noiseReduction:N(Math.round(_(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:N(de(t.auto_exposure_max_us,6e5),.01,.6),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:N(_(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:N(_(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(Ct(String(Math.round(_(t.fps,8)))),$t(`${Math.round(_(t.width,1280))}x${Math.round(_(t.height,720))}`),Pt(String(t.sampling_mode??"supersample")),te(!1)),Lt(N(Math.round(_(t.rotation,180)),0,270)),qt(!!t.flip_horizontal),Ut(!!t.flip_vertical),w(!1)},ea=async(t,r)=>{l(null);try{await b("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),qt(t),Ut(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},rr=async t=>{l(null);try{await b(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Lt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},ta=async t=>{l(null);try{await b(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},nr=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await b("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},sr=async()=>{l(null);try{await b("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},or=async()=>{l(null);try{await b("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ae=async()=>{L(!0);try{const t=await b("/api/debug/camera/presets",{cache:"no-store"});qa(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{L(!1)}},ir=async()=>{const t=nt.trim();if(t){L(!0),l(null);try{await b("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Vt.trim(),exposure_us:wt(d.exposureSeconds),analogue_gain:d.gain,digital_gain:d.digitalGain,auto_exposure:d.autoExposure,contrast:d.contrast,brightness:d.brightness,saturation:d.saturation,sharpness:d.sharpness,noise_reduction:d.noiseReduction,white_balance_mode:d.whiteBalanceMode,white_balance_gain_r:d.whiteBalanceGainR,white_balance_gain_b:d.whiteBalanceGainB,rotation:zt,flip_horizontal:Ce,flip_vertical:Fe,color_mode:d.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Xt(""),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},cr=async t=>{L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}},lr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},pt=async()=>{Yt(!0);try{const t=await b("/api/debug/files",{cache:"no-store"});Ua(t.files??[]),$e(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Yt(!1)}},ht=()=>{ct(null),ot(null),it(!1)},dr=async t=>{if(Re===t){ht();return}ct(t),ot(null),it(!0),l(null);try{const r=await b(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});ot(r)}catch(r){l(r instanceof Error?r.message:String(r)),ct(null)}finally{it(!1)}},ur=t=>{const r=(h,u)=>{const x=document.createElement("a");x.href=u,x.download=h,document.body.appendChild(x),x.click(),document.body.removeChild(x)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const u=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(u)}`)).ok)return;r(u,`${H("/files")}/${encodeURIComponent(u)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:u}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},mr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await b(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Re===t||(C==null?void 0:C.filename)===t)&&ht(),await pt()}catch(r){l(r instanceof Error?r.message:String(r))}}},xr=()=>{if(!Mt||!P.current||!Be.current)return;const t=P.current;if(!t.naturalWidth||!t.naturalHeight||(be.current||(be.current=document.createElement("canvas"),Pe.current=be.current.getContext("2d",{willReadFrequently:!0})),Me.current||(Me.current=Be.current.getContext("2d")),!Pe.current||!Me.current))return;const c=Math.min(1,320/t.naturalWidth),h=Math.max(1,Math.round(t.naturalWidth*c)),u=Math.max(1,Math.round(t.naturalHeight*c));be.current.width=h,be.current.height=u,Pe.current.drawImage(t,0,0,h,u);const x=Pe.current.getImageData(0,0,h,u).data,F=new Array(256).fill(0),y=new Array(256).fill(0),$=new Array(256).fill(0),M=new Array(256).fill(0);let z=0,oe=0,$a=0;const ie=h*u;for(let O=0;O=250&&($a+=1)}const vt=ie?z/ie:0,wr=ie?oe/ie-vt*vt:0;za({mean:vt,std:Math.sqrt(Math.max(0,wr)),over:ie?$a/ie*100:0});const Ue=Be.current,A=Me.current,We=window.devicePixelRatio||1,ve=Math.max(1,Ue.clientWidth||240),ye=Math.max(1,Ue.clientHeight||120);Ue.width=Math.floor(ve*We),Ue.height=Math.floor(ye*We),A.setTransform(We,0,0,We,0,0),A.clearRect(0,0,ve,ye);const Nr=Math.max(1,...Ze?[Math.max(...F),Math.max(...y),Math.max(...$)]:[0],...Qe?[Math.max(...M)]:[0]),Ve=(O,Xe)=>{A.beginPath(),A.strokeStyle=Xe,A.lineWidth=1.2;for(let W=0;W<256;W+=1){const je=W/255*ve,V=ye-O[W]/Nr*ye;W===0?A.moveTo(je,V):A.lineTo(je,V)}A.stroke()};if(Ze&&(Ve(F,"rgba(255,80,80,0.85)"),Ve(y,"rgba(80,255,80,0.85)"),Ve($,"rgba(80,160,255,0.85)")),Qe&&Ve(M,"rgba(255,255,255,0.95)"),Tt){const O=.9803921568627451*ve;A.fillStyle="rgba(255,100,100,0.12)",A.fillRect(O,0,ve-O,ye)}};o.useEffect(()=>{R(),Ae(),pt();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ee||ar(n.info,{syncRuntime:!xe})},[n==null?void 0:n.info,Ee,xe]),o.useEffect(()=>{if(!(n!=null&&n.recording)){q.current&&(window.clearInterval(q.current),q.current=null),Gt(0);return}if(q.current)return;const t=Date.now();return q.current=window.setInterval(()=>{Gt(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{q.current&&(window.clearInterval(q.current),q.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{Y.current=m,m||(ge(),at(!1),ne.current&&xt())},[m]),o.useEffect(()=>()=>{ne.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),ne.current=!1)},[]),o.useEffect(()=>{if(!k||!m)return;let t=!1;const r=async()=>{var h,u;if(!(dt.current||document.hidden)){dt.current=!0;try{const x=new URLSearchParams;j&&(x.set("target_x",j.x.toFixed(6)),x.set("target_y",j.y.toFixed(6)));const F=x.size>0?`?${x.toString()}`:"",y=await b(`${H("/camera/focus/metrics")}${F}`,{cache:"no-store"});if(t)return;Ot(y),D(null);const $=j?(h=y.selected_star)==null?void 0:h.hfd_px:(u=y.aggregate)==null?void 0:u.median_hfd_px;typeof $=="number"&&Number.isFinite($)&&(pe(M=>{var z;return((z=M.at(-1))==null?void 0:z.frameId)===y.frame_id?M:[...M,{frameId:y.frame_id,hfd:$}].slice(-60)}),he(M=>M==null?$:Math.min(M,$)))}catch(x){t||D(x instanceof Error?x.message:String(x))}finally{dt.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[k,m,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{m&&(U.current=[],se.current=0)},[E,m]),o.useEffect(()=>{if(!m){U.current=[],se.current=0,Se(0);return}if(!ut.current){const u=document.createElement("canvas");u.width=32,u.height=32,ut.current=u}const r=ut.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const h=()=>{const u=P.current;if(u!=null&&u.complete&&u.naturalWidth>0)try{r.drawImage(u,0,0,32,32);const x=r.getImageData(0,0,32,32).data;let F=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[m]),o.useEffect(()=>{if(!m){Se(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,h=U.current;for(;h.length&&h[0]window.clearInterval(t)},[m]),o.useEffect(()=>{if(!m){tt(null);return}let t=!1;const r=async()=>{try{const h=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!h.ok||t)return;const u=await h.json();if(!t){tt(u);const x=Number(u.preview_target_fps??u.target_preview_fps??0);Number.isFinite(x)&&x>0&&!xe&&Et(String(Math.round(x)))}}catch{t||tt(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[m,xe]),o.useEffect(()=>{if(!me)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[me]),o.useEffect(()=>()=>ge(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(fe.length/Je));lt>t&&$e(t)},[fe.length,lt]);const pr=m?`${H("/camera/stream")}?t=${E}`:"",Oe=d.autoExposure,ft=d.whiteBalanceMode==="manual",hr=N(parseInt(Ye,10)||Math.round(_((ua=n==null?void 0:n.info)==null?void 0:ua.fps,8)),1,60),aa=!d.autoExposure&&1/d.exposureSeconds0?Z.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],br=Array.isArray(Z.noise_reduction_modes)&&Z.noise_reduction_modes.length>0?Z.noise_reduction_modes:["off","fast","high_quality"],ra=Z.manual_digital_gain!==!1,na=!!((xa=n==null?void 0:n.info)!=null&&xa.night_mode),sa=m,He=!I&&!m&&!(n!=null&&n.recording),oa=!I&&m,gr=!I&&!kt&&sa,vr=!I&&!Ke&&sa,G=j?s==null?void 0:s.selected_star:null,De=j?(G==null?void 0:G.hfd_px)??null:((pa=s==null?void 0:s.aggregate)==null?void 0:pa.median_hfd_px)??null,ia=j?(G==null?void 0:G.fwhm_px)??null:((ha=s==null?void 0:s.aggregate)==null?void 0:ha.median_fwhm_px)??null,ca=j?(G==null?void 0:G.concentration)??null:((fa=s==null?void 0:s.aggregate)==null?void 0:fa.median_concentration)??null,Ge=De!=null&&re!=null&&re>0?(De-re)/re*100:null,la=_e.slice(-3).map(t=>t.hfd),bt=_e.slice(-6,-3).map(t=>t.hfd),gt=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let ze="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")ze="cam.focus.guidance.noStars";else if(_e.length>=5&&Ge!=null&&Ge<=3)ze="cam.focus.guidance.best";else if(la.length===3&&bt.length===3){const t=gt(la)-gt(bt),r=Math.max(.04,gt(bt)*.02);ze=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const yr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Le=Math.max(1,Math.ceil(fe.length/Je)),qe=Math.min(lt,Le),da=(qe-1)*Je,jr=fe.slice(da,da+Je);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Dr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Wr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void rr(t),className:`rounded border px-2 py-1 ${zt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void ea(!Ce,Fe),className:`rounded border px-2 py-1 ${Ce?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void ea(Ce,!Fe),className:`rounded border px-2 py-1 ${Fe?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void sr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void or(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ar,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void nr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ta(!0),className:`rounded border px-2 py-1 ${na?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Hr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ta(!1),className:`rounded border px-2 py-1 ${na?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Lr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:nt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Vt,onChange:t=>Xt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:st||!nt.trim(),onClick:()=>void ir(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Jt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Jt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",jt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:st,onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:st,onClick:()=>void lr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.cpu_usage)==null?"—":`${Number(p.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.memory_usage)==null?"—":`${Number(p.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.temperature)==null?"—":`${Number(p.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>k?void xt():void Qt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${k?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(yt,{className:"mr-1 inline h-3.5 w-3.5"}),a(ae?"cam.focus.starting":k?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(m?"cam.state.streaming":"cam.state.idle")]})]})]}),we&&!m&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${k?"cursor-crosshair":""}`,onClick:Xa,children:[m&&we&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),m?e.jsx("img",{ref:P,alt:"camera-preview",className:"h-full w-full object-contain",src:pr,onLoad:()=>{Q(null),ee(!1),xr()},onError:()=>{var r;if(ge(),!Y.current)return;const t=(r=P.current)==null?void 0:r.src;(async()=>{if(!Y.current||!t)return;if(await Aa()==="busy"){Q(a("cam.err.streamBusy")),ee(!0);return}Te.current=window.setTimeout(()=>{Y.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ba,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"mr-1 inline h-3.5 w-3.5"}),a(I?"cam.btn.starting":"cam.btn.start")]})]}),m&&k&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Pa,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Dt/60).toString().padStart(2,"0")}:${(Dt%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>At(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(It?"cam.hist.expand":"cam.hist.collapse")})}),!It&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Mt,onChange:t=>Oa(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ze,onChange:t=>Ha(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Qe,onChange:t=>Da(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Tt,onChange:t=>Ga(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Be,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:et.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:et.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[et.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${k?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(yt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${k?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(k?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),k?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[k?e.jsx("button",{type:"button",onClick:()=>void xt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>void Qt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(yt,{className:"mr-1 inline h-4 w-4"}),a(ae?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!k,onClick:Va,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Mr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!k||j==null,onClick:Ja,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),k?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:De==null?"—":De.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(ze)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:re==null?"—":`${re.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ge==null?"—":`+${Math.max(0,Ge).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(yr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ia==null?"—":`${ia.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ca==null?"—":`${(ca*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Xr,{samples:_e,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??La).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((ba=n==null?void 0:n.info)==null?void 0:ba.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((ga=n==null?void 0:n.info)==null?void 0:ga.actual_exposure_us)!=null?jt((i==null?void 0:i.actual_exposure_us)??((va=n==null?void 0:n.info)==null?void 0:va.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(ya=n==null?void 0:n.info)!=null&&ya.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:de(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Ie.current!=null?`${Math.max(0,Math.round((performance.now()-Ie.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ja=n==null?void 0:n.info)==null?void 0:ja.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((wa=n==null?void 0:n.info)==null?void 0:wa.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(Sa=n==null?void 0:n.info)!=null&&Sa.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ka=n==null?void 0:n.info)==null?void 0:ka.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((_a=n==null?void 0:n.info)==null?void 0:_a.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Ca=n==null?void 0:n.info)==null?void 0:Ca.width)??"—"}x${((Fa=n==null?void 0:n.info)==null?void 0:Fa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Ea=n==null?void 0:n.info)!=null&&Ea.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!oa,onClick:()=>void Ka(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${oa?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(zr,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!gr,onClick:()=>void Ya(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ba,{className:"inline h-4 w-4"})," ",a(kt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!vr,onClick:()=>void Za(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Pa,{className:"inline h-4 w-4"})," ",Ke?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Kt,onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Wa&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),C&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Ir,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:C.filename})]}),e.jsx("button",{type:"button",onClick:()=>ht(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx($r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ia(C.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:C.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(C.modified).toLocaleString()})]}),C.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:jt(C.exposure_us)})]}),C.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:C.analogue_gain})]}),C.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:C.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[fe.length===0&&!Kt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),jr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Re===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ia(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ur(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void dr(t.name),className:`rounded border px-2 py-1 ${Re===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void mr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Cr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:qe<=1,onClick:()=>$e(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:qe,total:Le})}),e.jsx("button",{type:"button",disabled:qe>=Le,onClick:()=>$e(t=>Math.min(Le,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:Ye,onChange:t=>{Ct(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:Ft,onChange:t=>{Et(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Bt,onChange:t=>{Pt(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Ur.map(t=>e.jsx("button",{type:"button",onClick:()=>{$t(t),te(!0)},className:`rounded border px-2 py-1 ${Rt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!xe,onClick:()=>void Qa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Gr,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ee&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Oe&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(Jr,{label:a("cam.controls.exposure"),value:d.exposureSeconds,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,exposureSeconds:t}))}}),e.jsx(X,{label:a("cam.controls.gain"),value:d.gain,min:1,max:24,step:.1,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.digitalGain"),value:d.digitalGain,min:1,max:8,step:.1,disabled:Oe||!ra,onChange:t=>{w(!0),S(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:d.noiseReductionMode,onChange:t=>{w(!0),S(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:br.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(X,{label:a("cam.controls.contrast"),value:d.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.brightness"),value:d.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),S(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.saturation"),value:d.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.sharpness"),value:d.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),aa!=null&&e.jsx("p",{className:"mt-2 text-[11px] text-tertiary",children:a("cam.controls.manualExposureFpsLimit",{exposure:d.exposureSeconds.toFixed(4),fps:aa.toFixed(2)})}),!ra&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ra=n==null?void 0:n.info)==null?void 0:Ra.actual_digital_gain)??d.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ee,onClick:()=>void er(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:d.autoExposure?"auto":"manual",onChange:t=>{S(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:d.colorMode,onChange:t=>{S(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:d.whiteBalanceMode,onChange:t=>{S(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:fr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:d.aeFlickerMode,onChange:t=>{S(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.6,step:.01,value:d.autoExposureMaxSeconds,onChange:t=>{S(r=>({...r,autoExposureMaxSeconds:N(Number(t.target.value)||.6,.01,.6)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(X,{label:"R Gain",value:d.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(X,{label:"B Gain",value:d.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!ft&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void tr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(ue||me)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[ue&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:ue}),me&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:me})]})]})}Sr.createRoot(document.getElementById("root")).render(e.jsx(kr.StrictMode,{children:e.jsx(Fr,{children:e.jsx(Pr,{children:e.jsx(Kr,{})})})})); + */const Lr=T("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),qr="/api/dev/debug";function H(a){return`${qr}${a}`}async function b(a,g={}){const f=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Br(f,g)}const Ur=["640x360","1280x720","1600x900","1920x1020"],Wr=[0,90,180,270],Je=12,ce=1e-4,le=.6,Vr=1e-4;function N(a,g,f){return Math.min(f,Math.max(g,a))}function _(a,g){return a==null||Number.isNaN(Number(a))?g:Number(a)}function de(a,g){return _(a,g)/1e6}function wt(a){return Math.round(a*1e6)}function jt(a){return`${de(a,0).toFixed(4)} s`}function Ta(a){const{exposureSeconds:g,autoExposureMaxSeconds:f,...p}=a;return{...p,exposure:wt(g),autoExposureMaxUs:wt(f)}}function Ia(a){if(!a)return"0 B";const g=["B","KB","MB","GB"];let f=0,p=a;for(;p>=1024&&fE.hfd),p=Math.min(...f),n=Math.max(...f),J=Math.max(.2,n-p),m=f.map((E,K)=>{const ue=K/Math.max(1,f.length-1)*320,l=76-(E-p)/J*60;return`${ue.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:m,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((B=m.split(" ").at(-1))==null?void 0:B.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Aa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const g=await a.json(),f=Number(g.max_clients??0),p=Number(g.active_clients??0);return f>0&&p>=f?"busy":"ok"}catch{return"fail"}}function X({label:a,value:g,min:f,max:p,step:n,onChange:J,disabled:m=!1,unit:B=""}){const E=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${m?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[g.toFixed(E),B]})]}),e.jsx("input",{type:"range",min:f,max:p,step:n,value:g,disabled:m,onChange:K=>J(Number(K.target.value)),className:"w-full accent-primary"})]})}function Jr({label:a,value:g,disabled:f,onChange:p}){const n=N(g,ce,le),J=Math.log10(ce),m=Math.log10(le);return e.jsxs("div",{className:`block ${f?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between gap-2",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("input",{type:"number",min:ce,max:le,step:Vr,value:Number(n.toFixed(6)),disabled:f,"aria-label":a,onChange:B=>{const E=Number(B.target.value);Number.isFinite(E)&&p(N(E,ce,le))},className:"w-24 rounded border border-outline-variant/30 bg-surface-container-low px-1.5 py-0.5 text-right font-mono text-[11px]"}),e.jsx("span",{className:"font-mono text-[11px]",children:"s"})]})]}),e.jsx("input",{type:"range",min:J,max:m,step:.001,value:Math.log10(n),disabled:f,"aria-label":a,onChange:B=>{const E=Number((10**Number(B.target.value)).toFixed(6));p(N(E,ce,le))},className:"w-full accent-primary"})]})}function Kr(){var ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a,Ca,Fa,Ea,Ra;const{t:a,locale:g,setLocale:f}=_r(),{info:p}=Er(),[n,J]=o.useState(null),[m,B]=o.useState(!1),[E,K]=o.useState(()=>Date.now()),[ue,l]=o.useState(null),[we,Q]=o.useState(null),[Nt,ee]=o.useState(!1),[me,v]=o.useState(null),[I,Ne]=o.useState(!1),[Ke,St]=o.useState(!1),[kt,_t]=o.useState(!1),[Ye,Ct]=o.useState("8"),[Ft,Et]=o.useState("8"),[Rt,$t]=o.useState("1280x720"),[Bt,Pt]=o.useState("supersample"),[xe,te]=o.useState(!1),[Mt,Oa]=o.useState(!0),[Ze,Ha]=o.useState(!0),[Qe,Da]=o.useState(!1),[Tt,Ga]=o.useState(!1),[It,At]=o.useState(!1),[et,za]=o.useState({mean:0,std:0,over:0}),[La,Se]=o.useState(0),[i,tt]=o.useState(null),[k,at]=o.useState(!1),[ae,ke]=o.useState(!1),[s,Ot]=o.useState(null),[_e,pe]=o.useState([]),[re,he]=o.useState(null),[j,rt]=o.useState(null),[Ht,D]=o.useState(null),[Dt,Gt]=o.useState(0),[zt,Lt]=o.useState(180),[Ce,qt]=o.useState(!1),[Fe,Ut]=o.useState(!1),[d,S]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ee,w]=o.useState(!1),[nt,Wt]=o.useState(""),[Vt,Xt]=o.useState(""),[Jt,qa]=o.useState([]),[st,L]=o.useState(!1),[fe,Ua]=o.useState([]),[Kt,Yt]=o.useState(!1),[C,ot]=o.useState(null),[Wa,it]=o.useState(!1),[Re,ct]=o.useState(null),[lt,$e]=o.useState(1),P=o.useRef(null),Be=o.useRef(null),be=o.useRef(null),Pe=o.useRef(null),Me=o.useRef(null),q=o.useRef(null),Te=o.useRef(null),dt=o.useRef(!1),ne=o.useRef(!1),Y=o.useRef(!1),Ie=o.useRef(null),U=o.useRef([]),se=o.useRef(0),ut=o.useRef(null),Zt=()=>{U.current=[],se.current=0,Ie.current=null,Se(0)},R=async()=>{try{const t=await b("/api/debug/camera/status",{cache:"no-store"});J(t),t.streaming||(B(!1),Y.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},ge=()=>{Te.current!=null&&(window.clearTimeout(Te.current),Te.current=null)},mt=async()=>{if(I)return!1;Ne(!0),l(null),Q(null),ee(!1);try{ge(),n!=null&&n.streaming||await b("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Aa();return r==="busy"?(Q(a("cam.err.streamBusy")),ee(!0),!1):r==="fail"?(Q(a("cam.err.streamProbeFailed")),ee(!1),!1):(B(!0),Y.current=!0,v(a("cam.notice.previewStart")),Zt(),Ie.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{Ne(!1)}},Qt=async()=>{if(!ae){ke(!0),D(null);try{if(!(m||await mt()))return;const r=await b(H("/camera/focus/session/start"),{method:"POST"});ne.current=r.active,S(c=>({...c,autoExposure:!1,exposureSeconds:de(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),Ot(null),pe([]),he(null),rt(null),At(!0),at(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},xt=async()=>{if(!ae){ke(!0),at(!1),D(null);try{if(ne.current){const t=await b(H("/camera/focus/session/stop"),{method:"POST"});ne.current=!1,t.restored_settings&&(S(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:de(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},Va=()=>{pe([]),he(null),D(null)},Xa=t=>{if(!k||!P.current||t.target.closest("button, label, input"))return;const r=P.current,c=t.currentTarget.getBoundingClientRect(),h=r.naturalWidth||(s==null?void 0:s.frame.width)||0,u=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(h<=0||u<=0)return;const x=Math.min(c.width/h,c.height/u),F=h*x,y=u*x,$=(c.width-F)/2,M=(c.height-y)/2,z=(t.clientX-c.left-$)/F,oe=(t.clientY-c.top-M)/y;z<0||z>1||oe<0||oe>1||(rt({x:z,y:oe}),pe([]),he(null),D(null))},Ja=()=>{rt(null),pe([]),he(null),D(null)},Ka=async()=>{if(!I){Ne(!0),l(null);try{ge(),B(!1),Y.current=!1,Q(null),ee(!1),Zt(),P.current&&(P.current.onload=null,P.current.onerror=null,P.current.src="",P.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},Ya=async()=>{if(!(!m&&!(n!=null&&n.streaming))){_t(!0),l(null);try{const t=await b("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{_t(!1)}}},Za=async()=>{if(!Ke){St(!0),l(null);try{if(n!=null&&n.recording)await b("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await b("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{St(!1)}}},Qa=async()=>{l(null);try{const t=N(parseInt(Ye,10)||5,1,60);await b(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=N(parseInt(Ft,10)||8,1,30);await b(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,h]=Rt.split("x").map(u=>parseInt(u,10));c&&h&&await b(`/api/debug/camera/size?width=${c}&height=${h}`,{method:"POST"}),await b(`/api/debug/camera/sampling?mode=${encodeURIComponent(Bt)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),te(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},er=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},tr=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},ar=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;S({exposureSeconds:N(de(t.exposure_us,5e3),ce,le),gain:N(_(t.analogue_gain,1),1,24),digitalGain:N(_(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:N(_(t.contrast,1),0,2),brightness:N(_(t.brightness,0),-1,1),saturation:N(_(t.saturation,1),0,2),sharpness:N(_(t.sharpness,1),0,2),noiseReduction:N(Math.round(_(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:N(de(t.auto_exposure_max_us,6e5),.01,.6),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:N(_(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:N(_(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(Ct(String(Math.round(_(t.fps,8)))),$t(`${Math.round(_(t.width,1280))}x${Math.round(_(t.height,720))}`),Pt(String(t.sampling_mode??"supersample")),te(!1)),Lt(N(Math.round(_(t.rotation,180)),0,270)),qt(!!t.flip_horizontal),Ut(!!t.flip_vertical),w(!1)},ea=async(t,r)=>{l(null);try{await b("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),qt(t),Ut(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},rr=async t=>{l(null);try{await b(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Lt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},ta=async t=>{l(null);try{await b(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},nr=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await b("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},sr=async()=>{l(null);try{await b("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},or=async()=>{l(null);try{await b("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ae=async()=>{L(!0);try{const t=await b("/api/debug/camera/presets",{cache:"no-store"});qa(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{L(!1)}},ir=async()=>{const t=nt.trim();if(t){L(!0),l(null);try{await b("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Vt.trim(),exposure_us:wt(d.exposureSeconds),analogue_gain:d.gain,digital_gain:d.digitalGain,auto_exposure:d.autoExposure,contrast:d.contrast,brightness:d.brightness,saturation:d.saturation,sharpness:d.sharpness,noise_reduction:d.noiseReduction,white_balance_mode:d.whiteBalanceMode,white_balance_gain_r:d.whiteBalanceGainR,white_balance_gain_b:d.whiteBalanceGainB,rotation:zt,flip_horizontal:Ce,flip_vertical:Fe,color_mode:d.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Xt(""),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},cr=async t=>{L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}},lr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},pt=async()=>{Yt(!0);try{const t=await b("/api/debug/files",{cache:"no-store"});Ua(t.files??[]),$e(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Yt(!1)}},ht=()=>{ct(null),ot(null),it(!1)},dr=async t=>{if(Re===t){ht();return}ct(t),ot(null),it(!0),l(null);try{const r=await b(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});ot(r)}catch(r){l(r instanceof Error?r.message:String(r)),ct(null)}finally{it(!1)}},ur=t=>{const r=(h,u)=>{const x=document.createElement("a");x.href=u,x.download=h,document.body.appendChild(x),x.click(),document.body.removeChild(x)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const u=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(u)}`)).ok)return;r(u,`${H("/files")}/${encodeURIComponent(u)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:u}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},mr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await b(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Re===t||(C==null?void 0:C.filename)===t)&&ht(),await pt()}catch(r){l(r instanceof Error?r.message:String(r))}}},xr=()=>{if(!Mt||!P.current||!Be.current)return;const t=P.current;if(!t.naturalWidth||!t.naturalHeight||(be.current||(be.current=document.createElement("canvas"),Pe.current=be.current.getContext("2d",{willReadFrequently:!0})),Me.current||(Me.current=Be.current.getContext("2d")),!Pe.current||!Me.current))return;const c=Math.min(1,320/t.naturalWidth),h=Math.max(1,Math.round(t.naturalWidth*c)),u=Math.max(1,Math.round(t.naturalHeight*c));be.current.width=h,be.current.height=u,Pe.current.drawImage(t,0,0,h,u);const x=Pe.current.getImageData(0,0,h,u).data,F=new Array(256).fill(0),y=new Array(256).fill(0),$=new Array(256).fill(0),M=new Array(256).fill(0);let z=0,oe=0,$a=0;const ie=h*u;for(let O=0;O=250&&($a+=1)}const vt=ie?z/ie:0,wr=ie?oe/ie-vt*vt:0;za({mean:vt,std:Math.sqrt(Math.max(0,wr)),over:ie?$a/ie*100:0});const Ue=Be.current,A=Me.current,We=window.devicePixelRatio||1,ve=Math.max(1,Ue.clientWidth||240),ye=Math.max(1,Ue.clientHeight||120);Ue.width=Math.floor(ve*We),Ue.height=Math.floor(ye*We),A.setTransform(We,0,0,We,0,0),A.clearRect(0,0,ve,ye);const Nr=Math.max(1,...Ze?[Math.max(...F),Math.max(...y),Math.max(...$)]:[0],...Qe?[Math.max(...M)]:[0]),Ve=(O,Xe)=>{A.beginPath(),A.strokeStyle=Xe,A.lineWidth=1.2;for(let W=0;W<256;W+=1){const je=W/255*ve,V=ye-O[W]/Nr*ye;W===0?A.moveTo(je,V):A.lineTo(je,V)}A.stroke()};if(Ze&&(Ve(F,"rgba(255,80,80,0.85)"),Ve(y,"rgba(80,255,80,0.85)"),Ve($,"rgba(80,160,255,0.85)")),Qe&&Ve(M,"rgba(255,255,255,0.95)"),Tt){const O=.9803921568627451*ve;A.fillStyle="rgba(255,100,100,0.12)",A.fillRect(O,0,ve-O,ye)}};o.useEffect(()=>{R(),Ae(),pt();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ee||ar(n.info,{syncRuntime:!xe})},[n==null?void 0:n.info,Ee,xe]),o.useEffect(()=>{if(!(n!=null&&n.recording)){q.current&&(window.clearInterval(q.current),q.current=null),Gt(0);return}if(q.current)return;const t=Date.now();return q.current=window.setInterval(()=>{Gt(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{q.current&&(window.clearInterval(q.current),q.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{Y.current=m,m||(ge(),at(!1),ne.current&&xt())},[m]),o.useEffect(()=>()=>{ne.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),ne.current=!1)},[]),o.useEffect(()=>{if(!k||!m)return;let t=!1;const r=async()=>{var h,u;if(!(dt.current||document.hidden)){dt.current=!0;try{const x=new URLSearchParams;j&&(x.set("target_x",j.x.toFixed(6)),x.set("target_y",j.y.toFixed(6)));const F=x.size>0?`?${x.toString()}`:"",y=await b(`${H("/camera/focus/metrics")}${F}`,{cache:"no-store"});if(t)return;Ot(y),D(null);const $=j?(h=y.selected_star)==null?void 0:h.hfd_px:(u=y.aggregate)==null?void 0:u.median_hfd_px;typeof $=="number"&&Number.isFinite($)&&(pe(M=>{var z;return((z=M.at(-1))==null?void 0:z.frameId)===y.frame_id?M:[...M,{frameId:y.frame_id,hfd:$}].slice(-60)}),he(M=>M==null?$:Math.min(M,$)))}catch(x){t||D(x instanceof Error?x.message:String(x))}finally{dt.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[k,m,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{m&&(U.current=[],se.current=0)},[E,m]),o.useEffect(()=>{if(!m){U.current=[],se.current=0,Se(0);return}if(!ut.current){const u=document.createElement("canvas");u.width=32,u.height=32,ut.current=u}const r=ut.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const h=()=>{const u=P.current;if(u!=null&&u.complete&&u.naturalWidth>0)try{r.drawImage(u,0,0,32,32);const x=r.getImageData(0,0,32,32).data;let F=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[m]),o.useEffect(()=>{if(!m){Se(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,h=U.current;for(;h.length&&h[0]window.clearInterval(t)},[m]),o.useEffect(()=>{if(!m){tt(null);return}let t=!1;const r=async()=>{try{const h=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!h.ok||t)return;const u=await h.json();if(!t){tt(u);const x=Number(u.preview_target_fps??u.target_preview_fps??0);Number.isFinite(x)&&x>0&&!xe&&Et(String(Math.round(x)))}}catch{t||tt(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[m,xe]),o.useEffect(()=>{if(!me)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[me]),o.useEffect(()=>()=>ge(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(fe.length/Je));lt>t&&$e(t)},[fe.length,lt]);const pr=m?`${H("/camera/stream")}?t=${E}`:"",Oe=d.autoExposure,ft=d.whiteBalanceMode==="manual",hr=N(parseInt(Ye,10)||Math.round(_((ua=n==null?void 0:n.info)==null?void 0:ua.fps,8)),1,60),aa=!d.autoExposure&&1/d.exposureSeconds0?Z.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],br=Array.isArray(Z.noise_reduction_modes)&&Z.noise_reduction_modes.length>0?Z.noise_reduction_modes:["off","fast","high_quality"],ra=Z.manual_digital_gain!==!1,na=!!((xa=n==null?void 0:n.info)!=null&&xa.night_mode),sa=m,He=!I&&!m&&!(n!=null&&n.recording),oa=!I&&m,gr=!I&&!kt&&sa,vr=!I&&!Ke&&sa,G=j?s==null?void 0:s.selected_star:null,De=j?(G==null?void 0:G.hfd_px)??null:((pa=s==null?void 0:s.aggregate)==null?void 0:pa.median_hfd_px)??null,ia=j?(G==null?void 0:G.fwhm_px)??null:((ha=s==null?void 0:s.aggregate)==null?void 0:ha.median_fwhm_px)??null,ca=j?(G==null?void 0:G.concentration)??null:((fa=s==null?void 0:s.aggregate)==null?void 0:fa.median_concentration)??null,Ge=De!=null&&re!=null&&re>0?(De-re)/re*100:null,la=_e.slice(-3).map(t=>t.hfd),bt=_e.slice(-6,-3).map(t=>t.hfd),gt=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let ze="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")ze="cam.focus.guidance.noStars";else if(_e.length>=5&&Ge!=null&&Ge<=3)ze="cam.focus.guidance.best";else if(la.length===3&&bt.length===3){const t=gt(la)-gt(bt),r=Math.max(.04,gt(bt)*.02);ze=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const yr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Le=Math.max(1,Math.ceil(fe.length/Je)),qe=Math.min(lt,Le),da=(qe-1)*Je,jr=fe.slice(da,da+Je);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Dr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Wr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void rr(t),className:`rounded border px-2 py-1 ${zt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void ea(!Ce,Fe),className:`rounded border px-2 py-1 ${Ce?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void ea(Ce,!Fe),className:`rounded border px-2 py-1 ${Fe?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void sr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void or(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ar,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void nr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ta(!0),className:`rounded border px-2 py-1 ${na?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Hr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ta(!1),className:`rounded border px-2 py-1 ${na?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Lr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:nt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Vt,onChange:t=>Xt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:st||!nt.trim(),onClick:()=>void ir(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Jt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Jt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",jt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:st,onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:st,onClick:()=>void lr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.cpu_usage)==null?"—":`${Number(p.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.memory_usage)==null?"—":`${Number(p.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.temperature)==null?"—":`${Number(p.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>k?void xt():void Qt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${k?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(yt,{className:"mr-1 inline h-3.5 w-3.5"}),a(ae?"cam.focus.starting":k?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(m?"cam.state.streaming":"cam.state.idle")]})]})]}),we&&!m&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${k?"cursor-crosshair":""}`,onClick:Xa,children:[m&&we&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),m?e.jsx("img",{ref:P,alt:"camera-preview",className:"h-full w-full object-contain",src:pr,onLoad:()=>{Q(null),ee(!1),xr()},onError:()=>{var r;if(ge(),!Y.current)return;const t=(r=P.current)==null?void 0:r.src;(async()=>{if(!Y.current||!t)return;if(await Aa()==="busy"){Q(a("cam.err.streamBusy")),ee(!0);return}Te.current=window.setTimeout(()=>{Y.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ba,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"mr-1 inline h-3.5 w-3.5"}),a(I?"cam.btn.starting":"cam.btn.start")]})]}),m&&k&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Pa,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Dt/60).toString().padStart(2,"0")}:${(Dt%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>At(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(It?"cam.hist.expand":"cam.hist.collapse")})}),!It&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Mt,onChange:t=>Oa(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ze,onChange:t=>Ha(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Qe,onChange:t=>Da(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Tt,onChange:t=>Ga(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Be,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:et.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:et.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[et.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${k?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(yt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${k?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(k?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),k?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[k?e.jsx("button",{type:"button",onClick:()=>void xt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>void Qt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(yt,{className:"mr-1 inline h-4 w-4"}),a(ae?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!k,onClick:Va,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Mr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!k||j==null,onClick:Ja,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),k?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:De==null?"—":De.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(ze)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:re==null?"—":`${re.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ge==null?"—":`+${Math.max(0,Ge).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(yr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ia==null?"—":`${ia.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ca==null?"—":`${(ca*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Xr,{samples:_e,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??La).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((ba=n==null?void 0:n.info)==null?void 0:ba.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((ga=n==null?void 0:n.info)==null?void 0:ga.actual_exposure_us)!=null?jt((i==null?void 0:i.actual_exposure_us)??((va=n==null?void 0:n.info)==null?void 0:va.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(ya=n==null?void 0:n.info)!=null&&ya.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:de(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Ie.current!=null?`${Math.max(0,Math.round((performance.now()-Ie.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ja=n==null?void 0:n.info)==null?void 0:ja.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((wa=n==null?void 0:n.info)==null?void 0:wa.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(Sa=n==null?void 0:n.info)!=null&&Sa.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ka=n==null?void 0:n.info)==null?void 0:ka.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((_a=n==null?void 0:n.info)==null?void 0:_a.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Ca=n==null?void 0:n.info)==null?void 0:Ca.width)??"—"}x${((Fa=n==null?void 0:n.info)==null?void 0:Fa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Ea=n==null?void 0:n.info)!=null&&Ea.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!oa,onClick:()=>void Ka(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${oa?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(zr,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!gr,onClick:()=>void Ya(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ba,{className:"inline h-4 w-4"})," ",a(kt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!vr,onClick:()=>void Za(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Pa,{className:"inline h-4 w-4"})," ",Ke?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Kt,onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Wa&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),C&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Ir,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:C.filename})]}),e.jsx("button",{type:"button",onClick:()=>ht(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx($r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ia(C.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:C.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(C.modified).toLocaleString()})]}),C.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:jt(C.exposure_us)})]}),C.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:C.analogue_gain})]}),C.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:C.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[fe.length===0&&!Kt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),jr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Re===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ia(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ur(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void dr(t.name),className:`rounded border px-2 py-1 ${Re===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void mr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Cr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:qe<=1,onClick:()=>$e(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:qe,total:Le})}),e.jsx("button",{type:"button",disabled:qe>=Le,onClick:()=>$e(t=>Math.min(Le,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:Ye,onChange:t=>{Ct(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:Ft,onChange:t=>{Et(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Bt,onChange:t=>{Pt(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Ur.map(t=>e.jsx("button",{type:"button",onClick:()=>{$t(t),te(!0)},className:`rounded border px-2 py-1 ${Rt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!xe,onClick:()=>void Qa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Gr,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ee&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Oe&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(Jr,{label:a("cam.controls.exposure"),value:d.exposureSeconds,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,exposureSeconds:t}))}}),e.jsx(X,{label:a("cam.controls.gain"),value:d.gain,min:1,max:24,step:.1,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.digitalGain"),value:d.digitalGain,min:1,max:8,step:.1,disabled:Oe||!ra,onChange:t=>{w(!0),S(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:d.noiseReductionMode,onChange:t=>{w(!0),S(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:br.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(X,{label:a("cam.controls.contrast"),value:d.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.brightness"),value:d.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),S(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.saturation"),value:d.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.sharpness"),value:d.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),aa!=null&&e.jsx("p",{className:"mt-2 text-[11px] text-tertiary",children:a("cam.controls.manualExposureFpsLimit",{exposure:d.exposureSeconds.toFixed(4),fps:aa.toFixed(2)})}),!ra&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ra=n==null?void 0:n.info)==null?void 0:Ra.actual_digital_gain)??d.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ee,onClick:()=>void er(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:d.autoExposure?"auto":"manual",onChange:t=>{S(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:d.colorMode,onChange:t=>{S(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:d.whiteBalanceMode,onChange:t=>{S(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:fr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:d.aeFlickerMode,onChange:t=>{S(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.6,step:.01,value:d.autoExposureMaxSeconds,onChange:t=>{S(r=>({...r,autoExposureMaxSeconds:N(Number(t.target.value)||.6,.01,.6)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(X,{label:"R Gain",value:d.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(X,{label:"B Gain",value:d.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!ft&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void tr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(ue||me)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[ue&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:ue}),me&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:me})]})]})}Sr.createRoot(document.getElementById("root")).render(e.jsx(kr.StrictMode,{children:e.jsx(Fr,{children:e.jsx(Pr,{children:e.jsx(Kr,{})})})})); diff --git a/web/static/analysis-lab/camera.html b/web/static/analysis-lab/camera.html index c7ecb9d..21fc380 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,7 +4,7 @@ OGScope 相机调试控制台 - + From d89f5d612b3fcdc40601d430613793dee6c9b369 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: Thu, 27 Aug 2026 17:42:50 +0800 Subject: [PATCH 16/24] =?UTF-8?q?fix:=20=E7=A8=B3=E5=AE=9A=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E4=B8=8E=E6=9B=9D?= =?UTF-8?q?=E5=85=89=E5=9F=BA=E7=BA=BF=E6=81=A2=E5=A4=8D=20/=20stabilize?= =?UTF-8?q?=20camera=20lifecycle=20and=20exposure=20baseline=20restore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ogscope/core/application/core_service.py | 30 ++++++++++---- tests/unit/test_core_contract_api.py | 50 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/ogscope/core/application/core_service.py b/ogscope/core/application/core_service.py index 20fea4e..a3927b1 100644 --- a/ogscope/core/application/core_service.py +++ b/ogscope/core/application/core_service.py @@ -334,8 +334,10 @@ async def get_camera_status(self) -> dict[str, Any]: async def start_camera(self) -> dict[str, Any]: """启动 Core 相机 / Start core camera.""" hardware_client = get_hardware_plane_client() - hp_result = await hardware_client.device_command("camera", "start") + # 冷启动可能超过硬件平面通用 RPC 预算;先完成真实相机启动,再同步平面状态。 + # Cold start can exceed the generic plane RPC budget; start hardware first, then sync plane state. result = await camera_domain_service.start() + hp_result = await hardware_client.device_command("camera", "start") status = await self.get_camera_status() ready = bool(status.get("connected")) and bool(status.get("streaming")) message = str(result.get("message", "")) @@ -359,8 +361,10 @@ async def start_camera(self) -> dict[str, Any]: async def stop_camera(self) -> dict[str, Any]: """停止 Core 相机 / Stop core camera.""" hardware_client = get_hardware_plane_client() - hp_result = await hardware_client.device_command("camera", "stop") + # 先让相机域完成有界清理,避免硬件平面短 RPC 预算提前取消释放流程。 + # Let the camera domain finish bounded cleanup before the short plane RPC can cancel it. result = await camera_domain_service.stop() + hp_result = await hardware_client.device_command("camera", "stop") return { "success": bool(result.get("success", True)), "message": result.get("message", ""), @@ -375,15 +379,25 @@ async def tune_camera(self, payload: dict[str, Any]) -> dict[str, Any]: """按 Core 语义微调相机参数 / Tune camera params with core semantics.""" applied: dict[str, Any] = {} auto_exposure = payload.get("auto_exposure") - if auto_exposure is not None: + exposure_us = payload.get("exposure_us") + restore_auto_after_exposure = bool(auto_exposure) and exposure_us is not None + + if auto_exposure is not None and not restore_auto_after_exposure: await camera_domain_service.set_auto_exposure_mode(bool(auto_exposure)) applied["auto_exposure"] = bool(auto_exposure) - if payload.get("exposure_us") is not None: - await camera_domain_service.update_settings( - {"exposure": payload["exposure_us"]} - ) - applied["exposure_us"] = int(payload["exposure_us"]) + if exposure_us is not None: + exposure_settings: dict[str, Any] = {"exposure": exposure_us} + if restore_auto_after_exposure: + # 先在手动模式写入可恢复基线,再重新开启 AE。 + # Write the restorable manual baseline before re-enabling AE. + exposure_settings["autoExposure"] = False + await camera_domain_service.update_settings(exposure_settings) + applied["exposure_us"] = int(exposure_us) + + if restore_auto_after_exposure: + await camera_domain_service.set_auto_exposure_mode(True) + applied["auto_exposure"] = True if payload.get("analogue_gain") is not None: settings: dict[str, Any] = {"gain": float(payload["analogue_gain"])} diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index c6acf05..c858eb1 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -88,12 +88,16 @@ async def test_core_camera_start_requires_confirmed_ready(monkeypatch) -> None: """启动成功仍须确认真实流就绪 / A successful command still requires stream readiness.""" from ogscope.core.application import core_service + events: list[str] = [] + class _HardwareClient: async def device_command(self, service: str, command: str) -> dict: assert (service, command) == ("camera", "start") + events.append("hardware_plane") return {"success": True} async def _start() -> dict: + events.append("camera_domain") return {"success": True, "message": "start accepted"} async def _not_ready() -> dict: @@ -112,6 +116,7 @@ async def _not_ready() -> dict: result = await service.start_camera() + assert events == ["camera_domain", "hardware_plane"] assert result["success"] is False assert result["info"]["model"] == "IMX327" assert result["applied"] == { @@ -124,6 +129,51 @@ async def _not_ready() -> dict: assert result["message"] == "camera did not report a valid stream" +@pytest.mark.unit +@pytest.mark.asyncio +async def test_core_camera_tune_writes_manual_baseline_before_enabling_ae( + monkeypatch, +) -> None: + """同时恢复 AE 时先写手动基线 / Combined AE restore writes the manual baseline first.""" + from ogscope.core.application import core_service + + events: list[tuple[str, object]] = [] + + async def _update_settings(settings: dict) -> dict: + events.append(("settings", settings)) + return {"success": True} + + async def _set_auto_exposure(enabled: bool) -> dict: + events.append(("auto_exposure", enabled)) + return {"success": True} + + async def _status() -> dict: + return {"info": {"exposure_us": 600_000, "auto_exposure": True}} + + monkeypatch.setattr( + core_service.camera_domain_service, "update_settings", _update_settings + ) + monkeypatch.setattr( + core_service.camera_domain_service, + "set_auto_exposure_mode", + _set_auto_exposure, + ) + monkeypatch.setattr(core_service.camera_domain_service, "get_status", _status) + + result = await core_service.CoreContractService().tune_camera( + {"exposure_us": 600_000, "auto_exposure": True} + ) + + assert events == [ + ("settings", {"exposure": 600_000, "autoExposure": False}), + ("auto_exposure", True), + ] + assert result["applied"] == { + "exposure_us": 600_000, + "auto_exposure": True, + } + + @pytest.mark.unit def test_core_system_status_network_delegated_when_subordinate(monkeypatch) -> None: """subordinate 下 network 标记 delegated 且不降级 / Subordinate marks network delegated.""" From f8d784e77e39d3af0201f91d5b1e41a6fd017750 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: Thu, 27 Aug 2026 17:48:43 +0800 Subject: [PATCH 17/24] =?UTF-8?q?fix:=20=E6=8E=92=E9=99=A4=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=A0=91Git=E6=8C=87=E9=92=88=E6=96=87=E4=BB=B6=20/?= =?UTF-8?q?=20exclude=20worktree=20Git=20pointer=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/sync_board_code.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sync_board_code.sh b/scripts/sync_board_code.sh index a01d590..fc245e6 100755 --- a/scripts/sync_board_code.sh +++ b/scripts/sync_board_code.sh @@ -45,7 +45,7 @@ echo "== Sync OGScope code → ${REMOTE}:${DEV_PATH} (uploads/logs/data preserve rsync -avz --delete \ -e "${RSYNC_SSH}" \ - --exclude '.git/' \ + --exclude '.git' \ --exclude '.venv/' \ --exclude 'node_modules/' \ --exclude '__pycache__/' \ From bcf0e4c14d47314ac08b18804b41205bbabe0931 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: Thu, 27 Aug 2026 18:00:43 +0800 Subject: [PATCH 18/24] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=98=BB?= =?UTF-8?q?=E6=96=AD=E5=8F=91=E5=B8=83=E7=9A=84=E5=A4=B1=E6=95=88=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E9=93=BE=E6=8E=A5=20/=20remove=20broken=20protocol=20?= =?UTF-8?q?link=20blocking=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ogscope/vendor/tetra3/proto/cedar_detect.proto | 1 - 1 file changed, 1 deletion(-) delete mode 120000 ogscope/vendor/tetra3/proto/cedar_detect.proto diff --git a/ogscope/vendor/tetra3/proto/cedar_detect.proto b/ogscope/vendor/tetra3/proto/cedar_detect.proto deleted file mode 120000 index dfa0615..0000000 --- a/ogscope/vendor/tetra3/proto/cedar_detect.proto +++ /dev/null @@ -1 +0,0 @@ -../../../cedar-detect/src/proto/cedar_detect.proto \ No newline at end of file From 973816392f367ea7608a90be50b5b81302e97e31 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: Thu, 27 Aug 2026 18:04:04 +0800 Subject: [PATCH 19/24] =?UTF-8?q?fix:=20=E6=8E=88=E6=9D=83=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=88=9B=E5=BB=BA=E7=89=88?= =?UTF-8?q?=E6=9C=AC=20/=20allow=20release=20workflow=20to=20publish=20rel?= =?UTF-8?q?eases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1bb87f..bcd1393 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' +permissions: + contents: write + jobs: release: runs-on: ubuntu-latest @@ -23,7 +26,7 @@ jobs: run: poetry build - name: 创建 GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: dist/* generate_release_notes: true @@ -35,4 +38,3 @@ jobs: # run: poetry publish # env: # POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} - From 50271fad5cbafebc89a92c53cd71fe7540033ed2 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: Mon, 31 Aug 2026 21:51:12 +0800 Subject: [PATCH 20/24] =?UTF-8?q?fix:=20=E5=86=85=E7=BD=AE=20IMX327=20?= =?UTF-8?q?=E4=B8=80=E7=A7=92=E8=87=AA=E5=8A=A8=E6=9B=9D=E5=85=89=E8=B0=83?= =?UTF-8?q?=E6=A0=A1=20/=20bundle=20one-second=20IMX327=20auto-exposure=20?= =?UTF-8?q?tuning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/ogscope.env.example | 3 +- docs/DEBUG_CONSOLE.md | 5 +- docs/DEBUG_CONSOLE_EN.md | 5 +- ogscope/config.py | 15 +- ogscope/config_catalog.py | 1 + ogscope/platform/hardware/camera.py | 190 +++++++++++++--- ogscope/platform/hardware/tuning/imx327.json | 215 ++++++++++++++++++ ogscope/web/camera_shared.py | 7 +- pyproject.toml | 1 + tests/unit/test_camera_flip.py | 10 +- tests/unit/test_camera_tuning.py | 145 ++++++++++++ tests/unit/test_config_catalog.py | 7 +- web/spa/src/apps/camera/CameraConsoleApp.tsx | 21 +- .../analysis-lab/assets/camera-C3Lweuht.js | 61 +++++ .../analysis-lab/assets/camera-q9IdHzgf.js | 61 ----- web/static/analysis-lab/camera.html | 2 +- 16 files changed, 631 insertions(+), 118 deletions(-) create mode 100644 ogscope/platform/hardware/tuning/imx327.json create mode 100644 tests/unit/test_camera_tuning.py create mode 100644 web/static/analysis-lab/assets/camera-C3Lweuht.js delete mode 100644 web/static/analysis-lab/assets/camera-q9IdHzgf.js diff --git a/deploy/ogscope.env.example b/deploy/ogscope.env.example index 63cef90..d4839b9 100644 --- a/deploy/ogscope.env.example +++ b/deploy/ogscope.env.example @@ -43,7 +43,8 @@ OGSCOPE_RELOAD=false # OGSCOPE_CAMERA_AE_POLAR_PRESET=true # OGSCOPE_CAMERA_AE_EXPOSURE_VALUE=1.0 # OGSCOPE_CAMERA_AE_AGGRESSIVE_ENABLED=true -# OGSCOPE_CAMERA_AUTO_EXPOSURE_MAX_US=600000 +# OGSCOPE_CAMERA_AUTO_EXPOSURE_MAX_US=1000000 +# OGSCOPE_CAMERA_TUNING_FILE=/path/to/imx327-override.json # OGSCOPE_CAMERA_FLIP_HORIZONTAL=false # OGSCOPE_CAMERA_FLIP_VERTICAL=false diff --git a/docs/DEBUG_CONSOLE.md b/docs/DEBUG_CONSOLE.md index 43c6427..7566735 100644 --- a/docs/DEBUG_CONSOLE.md +++ b/docs/DEBUG_CONSOLE.md @@ -27,7 +27,7 @@ OGScope 调试控制台是一个专为开发者设计的相机调试工具,提 - **参数记录**: 每次拍摄自动生成参数记录文件 ### ⚙️ 参数设置 -- **曝光时间**: 0.1ms–0.6s(微秒级调节,与自动曝光上限一致) +- **曝光时间**: 0.1ms–1s(微秒级调节,与自动曝光上限一致) - **模拟增益**: 1x - 16x (0.1x步进) - **数字增益**: 1x - 4x (0.1x步进) - **白平衡**: `auto` / `manual` / `night`,手动模式可设置红/蓝增益 @@ -238,7 +238,8 @@ python scripts/test_debug_console.py --test deps | `camera_white_balance_mode` | `auto` | `auto` / `manual` / `night` | | `camera_white_balance_gain_r` / `camera_white_balance_gain_b` | `1.0` | 手动白平衡红/蓝增益 | | `camera_night_mode` | `false` | 启动时应用夜间白平衡标记 | -| `camera_auto_exposure_max_us` | `600000` | 自动曝光最长帧周期,暗场允许降至约 1.7 fps | +| `camera_tuning_file` | 空 | 可选 Picamera2 tuning 覆盖路径;默认加载产品内置 IMX327 tuning | +| `camera_auto_exposure_max_us` | `1000000` | 自动曝光最长帧周期,暗场允许降至约 1 fps | | `camera_ae_flicker_mode` | `off` | `off` / `50hz` / `60hz` | | `camera_noise_reduction_mode` | `fast` | `off` / `fast` / `high_quality` | | `camera_lores_enabled` | `true` | 启用低分辨率辅助流统计 | diff --git a/docs/DEBUG_CONSOLE_EN.md b/docs/DEBUG_CONSOLE_EN.md index 52b01c1..0d709f5 100644 --- a/docs/DEBUG_CONSOLE_EN.md +++ b/docs/DEBUG_CONSOLE_EN.md @@ -27,7 +27,7 @@ The OGScope debug console is a developer-focused camera tool: live preview, capt - **Sidecar metadata**: `.txt` parameter file per capture ### Parameters -- **Exposure**: 0.1ms–0.6s (fine steps, aligned with the auto-exposure ceiling) +- **Exposure**: 0.1ms–1s (fine steps, aligned with the auto-exposure ceiling) - **Analog gain**: 1x–16x (0.1x steps) - **Digital gain**: 1x–4x (0.1x steps) - **White balance**: `auto` / `manual` / `night`; manual mode exposes red/blue gains @@ -188,7 +188,8 @@ These settings enter runtime through environment variables or config files. Name | `camera_white_balance_mode` | `auto` | `auto` / `manual` / `night` | | `camera_white_balance_gain_r` / `camera_white_balance_gain_b` | `1.0` | Manual white-balance red/blue gains | | `camera_night_mode` | `false` | Apply night white-balance flag at startup | -| `camera_auto_exposure_max_us` | `600000` | Longest AE frame duration; dark scenes may fall to about 1.7 fps | +| `camera_tuning_file` | empty | Optional Picamera2 tuning override path; the bundled IMX327 tuning is used by default | +| `camera_auto_exposure_max_us` | `1000000` | Longest AE frame duration; dark scenes may fall to about 1 fps | | `camera_ae_flicker_mode` | `off` | `off` / `50hz` / `60hz` | | `camera_noise_reduction_mode` | `fast` | `off` / `fast` / `high_quality` | | `camera_lores_enabled` | `true` | Enable the low-resolution helper stream | diff --git a/ogscope/config.py b/ogscope/config.py index 39d0fbd..d3e72f3 100644 --- a/ogscope/config.py +++ b/ogscope/config.py @@ -114,11 +114,18 @@ class Settings(BaseSettings): default=True, description="根据暗部与高光占比动态提高极轴镜 AE / Dynamically bias polar AE toward dark detail", ) + camera_tuning_file: Optional[Path] = Field( + default=None, + description=( + "Picamera2 tuning 覆盖路径;为空时使用随产品发布的 IMX327 tuning / " + "Picamera2 tuning override path; bundled IMX327 tuning is used by default" + ), + ) camera_auto_exposure_max_us: int = Field( - default=600_000, + default=1_000_000, ge=10_000, le=10_000_000, - description="自动曝光最长帧周期 600ms,暗场允许降帧 / Max auto-exposure frame duration, capped at 600ms", + description="自动曝光最长帧周期 1s,暗场允许降帧 / Max auto-exposure frame duration, capped at 1s", ) camera_ae_flicker_mode: str = Field( default="off", @@ -540,9 +547,9 @@ def _parse_camera_white_balance_mode(cls, value: object) -> str: @field_validator("camera_auto_exposure_max_us", mode="before") @classmethod def _cap_camera_auto_exposure_max_us(cls, value: object) -> object: - """兼容旧配置并限制暗场曝光为 600ms / Keep legacy config bootable and cap AE at 600ms.""" + """兼容旧配置并限制暗场曝光为 1s / Keep legacy config bootable and cap AE at 1s.""" try: - return min(600_000, int(value)) + return min(1_000_000, int(value)) except (TypeError, ValueError): return value diff --git a/ogscope/config_catalog.py b/ogscope/config_catalog.py index 28c7307..60a4115 100644 --- a/ogscope/config_catalog.py +++ b/ogscope/config_catalog.py @@ -69,6 +69,7 @@ "camera_ae_polar_preset", "camera_ae_exposure_value", "camera_ae_aggressive_enabled", + "camera_tuning_file", "camera_auto_exposure_max_us", "camera_ae_flicker_mode", "camera_noise_reduction_mode", diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 12e28ac..696cf33 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -5,9 +5,11 @@ """ import logging +import math import time from abc import ABC, abstractmethod from concurrent.futures import TimeoutError as FutureTimeoutError +from pathlib import Path from typing import Any, Optional import numpy as np @@ -64,6 +66,9 @@ class IMX327MIPICamera(CameraInterface): PREVIEW_BUFFER_COUNT = 2 AE_SUPERVISOR_INTERVAL_S = 1.0 AE_SUPERVISOR_STEP_EV = 0.5 + AUTO_EXPOSURE_MAX_US = 1_000_000 + LUMINANCE_STATS_MAX_SAMPLES = 32_768 + PRODUCT_TUNING_FILE = Path(__file__).with_name("tuning") / "imx327.json" MANUAL_CONTROL_RANGE_DEFAULTS = { "ExposureTime": {"min": 1000, "max": 100000, "default": 10000, "step": 1000}, "AnalogueGain": {"min": 1.0, "max": 16.0, "default": 1.0, "step": 0.1}, @@ -80,9 +85,19 @@ def __init__(self, config: dict[str, Any]): self.backend_name = "picamera2/libcamera" self.output_pixel_format = "RGB888" self._frame_duration_limits: tuple[int, int] | None = None + self._frame_duration_control = "not_applied" + self._frame_duration_control_error: str | None = None self._lores_available = False self._last_lores_stats: dict[str, Any] = {} self._pending_capture_job = None + self.tuning_file = ( + Path(str(config["tuning_file"])).expanduser() + if config.get("tuning_file") + else None + ) + self._tuning_source = "system_default" + self._tuning_loaded = False + self._tuning_error: str | None = None # 相机参数 / Camera parameters requested_width = int(config.get("width", 640)) @@ -106,7 +121,11 @@ def __init__(self, config: dict[str, Any]): self.white_balance_gain_b = config.get("white_balance_gain_b", 1.0) self.night_mode = bool(config.get("night_mode", False)) self.auto_exposure_max_us = max( - 10_000, min(600_000, int(config.get("auto_exposure_max_us", 600_000))) + 10_000, + min( + self.AUTO_EXPOSURE_MAX_US, + int(config.get("auto_exposure_max_us", self.AUTO_EXPOSURE_MAX_US)), + ), ) self.capture_timeout_sec = max( 0.5, float(config.get("capture_timeout_sec", 4.0)) @@ -395,19 +414,26 @@ def _compute_frame_duration_limits(self) -> tuple[int, int]: def _apply_frame_duration_controls(self) -> None: """优先应用 FrameDurationLimits,失败时回退 FrameRate / Prefer FrameDurationLimits, fallback to FrameRate.""" if not self.camera: + self._frame_duration_control = "camera_unavailable" return limits = self._compute_frame_duration_limits() self._frame_duration_limits = limits + self._frame_duration_control_error = None if self._control_supported("FrameDurationLimits"): try: self.camera.set_controls({"FrameDurationLimits": limits}) + self._frame_duration_control = "frame_duration_limits" return except Exception as e: + self._frame_duration_control_error = type(e).__name__ logger.debug("FrameDurationLimits 未生效,回退 FrameRate: %s", e) try: self.camera.set_controls({"FrameRate": float(self.fps)}) - except Exception: - pass + self._frame_duration_control = "frame_rate_fallback" + except Exception as e: + self._frame_duration_control = "failed" + self._frame_duration_control_error = type(e).__name__ + logger.debug("帧周期控制未生效 / Frame-duration control failed: %s", e) def _noise_reduction_control_value(self) -> Any: """将语义模式映射到 libcamera 降噪枚举/整数 / Map semantic NR mode to libcamera value.""" @@ -499,37 +525,84 @@ def _create_video_configuration(self) -> Any: buffer_count=self.PREVIEW_BUFFER_COUNT, ) - def _collect_lores_stats(self, request: Any) -> None: - """从 lores 流提取轻量亮度统计 / Extract lightweight luminance stats from lores stream.""" - if not self._lores_available: + @staticmethod + def _histogram_percentile(histogram: np.ndarray, percentile: float) -> int: + """从 8 位直方图读取最近秩百分位 / Read nearest-rank percentile from an 8-bit histogram.""" + total = int(histogram.sum()) + if total <= 0: + return 0 + rank = max(1, int(math.ceil(total * percentile / 100.0))) + return int(np.searchsorted(np.cumsum(histogram), rank, side="left")) + + def _luminance_stats(self, luminance: np.ndarray, *, source: str) -> dict[str, Any]: + """用有界采样直方图计算亮度统计 / Compute luminance stats with a bounded histogram sample.""" + values = np.asarray(luminance) + if values.ndim >= 2 and values.size > self.LUMINANCE_STATS_MAX_SAMPLES: + stride = max( + 1, + int( + math.ceil(math.sqrt(values.size / self.LUMINANCE_STATS_MAX_SAMPLES)) + ), + ) + values = values[::stride, ::stride] + values = np.clip(values.reshape(-1), 0, 255).astype(np.uint8, copy=False) + histogram = np.bincount(values, minlength=256) + sample_count = int(histogram.sum()) + if sample_count <= 0: + return {} + levels = np.arange(256, dtype=np.float64) + return { + "source": source, + "sample_count": sample_count, + "mean": float(np.dot(histogram, levels) / sample_count), + "min": int(np.flatnonzero(histogram)[0]), + "max": int(np.flatnonzero(histogram)[-1]), + "p50": float(self._histogram_percentile(histogram, 50.0)), + "p90": float(self._histogram_percentile(histogram, 90.0)), + "p99": float(self._histogram_percentile(histogram, 99.0)), + "p99_8": float(self._histogram_percentile(histogram, 99.8)), + "saturated_fraction": float(histogram[250:].sum() / sample_count), + } + + def _collect_lores_stats( + self, request: Any, main_image: np.ndarray | None = None + ) -> None: + """优先从 lores 提取统计,失败时有界采样主流 / Prefer lores stats; bounded main-stream fallback.""" + luminance: np.ndarray | None = None + source = "main_fallback" + if self._lores_available: + try: + lores = request.make_array("lores") + if lores is not None: + if ( + len(getattr(lores, "shape", ())) == 2 + and lores.shape[0] >= self.lores_height + ): + luminance = lores[: self.lores_height, :] + elif len(getattr(lores, "shape", ())) >= 3: + luminance = lores[..., 0] + else: + luminance = lores + source = "lores" + except Exception as e: + logger.debug( + "读取 lores 统计失败,回退主流 / Lores stats failed; using main stream: %s", + e, + ) + if luminance is None and main_image is not None: + # RGB 主流用绿色通道作为低分配亮度近似;曝光监督不需要精确色度转换。 + # Use green as a low-allocation RGB luminance proxy; AE supervision does not need chroma accuracy. + luminance = main_image[..., 1] if main_image.ndim >= 3 else main_image + if luminance is None: return try: - lores = request.make_array("lores") - if lores is None: + stats = self._luminance_stats(luminance, source=source) + if not stats: return - if ( - len(getattr(lores, "shape", ())) == 2 - and lores.shape[0] >= self.lores_height - ): - y_plane = lores[: self.lores_height, :] - elif len(getattr(lores, "shape", ())) >= 3: - y_plane = lores[..., 0] - else: - y_plane = lores - p50, p90, p99 = np.percentile(y_plane, [50, 90, 99]) - saturated_fraction = float(np.mean(y_plane >= 250)) - self._last_lores_stats = { - "mean": float(np.mean(y_plane)), - "min": int(np.min(y_plane)), - "max": int(np.max(y_plane)), - "p50": float(p50), - "p90": float(p90), - "p99": float(p99), - "saturated_fraction": saturated_fraction, - } - self._update_aggressive_auto_exposure(self._last_lores_stats) + self._last_lores_stats = stats + self._update_aggressive_auto_exposure(stats) except Exception as e: - logger.debug("读取 lores 统计失败 / Failed to read lores stats: %s", e) + logger.debug("亮度统计失败 / Failed to compute luminance stats: %s", e) def _desired_aggressive_ae_ev(self, stats: dict[str, Any]) -> float: """按暗部与高光占比选择 AE 补偿 / Select AE bias from shadows and highlight share.""" @@ -724,12 +797,56 @@ def _apply_white_balance_controls(self) -> None: return self.camera.set_controls(self._white_balance_controls()) + def _load_picamera_tuning(self, picamera_type: Any) -> dict[str, Any] | None: + """加载产品或显式覆盖 tuning,失败时允许系统默认回退 / Load product or override tuning with safe fallback.""" + tuning_path = self.tuning_file or self.PRODUCT_TUNING_FILE + source_prefix = "override" if self.tuning_file else "product" + self._tuning_source = f"{source_prefix}:{tuning_path.name}" + loader = getattr(picamera_type, "load_tuning_file", None) + if not callable(loader): + self._tuning_source = "system_default" + self._tuning_error = "load_tuning_file_unavailable" + return None + try: + tuning = loader(tuning_path.name, dir=str(tuning_path.parent)) + self._tuning_loaded = True + self._tuning_error = None + return tuning + except Exception as e: + # API 只报告错误类型,完整路径只进入本机日志,避免泄露部署目录。 + # The API reports only the error type; the full local path stays in logs. + self._tuning_source = "system_default" + self._tuning_loaded = False + self._tuning_error = type(e).__name__ + logger.warning( + "相机 tuning 加载失败,回退系统默认 / Camera tuning load failed; using system default (%s): %s", + tuning_path, + e, + ) + return None + def initialize(self) -> bool: """初始化 MIPI 相机 / Initialize MIPI camera""" try: from picamera2 import Picamera2 - self.camera = Picamera2() + tuning = self._load_picamera_tuning(Picamera2) + if tuning is None: + self.camera = Picamera2() + else: + try: + self.camera = Picamera2(tuning=tuning) + except TypeError as e: + # 旧版 Picamera2 构造器可能不支持 tuning 参数,保持可启动并明确降级。 + # Older Picamera2 constructors may reject tuning; keep startup viable and report fallback. + self._tuning_source = "system_default" + self._tuning_loaded = False + self._tuning_error = type(e).__name__ + logger.warning( + "Picamera2 不接受产品 tuning,回退系统默认 / Picamera2 rejected product tuning; using default: %s", + e, + ) + self.camera = Picamera2() # 配置主流 + 可选 lores 流;RGB888 保证预览/解算色序一致 # Configure main + optional lores stream; RGB888 keeps preview/solve color order stable. @@ -871,7 +988,7 @@ def capture_image(self) -> Optional[np.ndarray]: try: image = request.make_array("main") self._last_metadata = dict(request.get_metadata() or {}) - self._collect_lores_stats(request) + self._collect_lores_stats(request, main_image=image) finally: request.release() @@ -1262,6 +1379,8 @@ def get_camera_info(self) -> dict[str, Any]: "frame_duration_limits": list( self._frame_duration_limits or self._compute_frame_duration_limits() ), + "frame_duration_control": self._frame_duration_control, + "frame_duration_control_error": self._frame_duration_control_error, "analogue_gain": self.analogue_gain, "actual_analogue_gain": metadata.get( "AnalogueGain", self.analogue_gain @@ -1270,6 +1389,9 @@ def get_camera_info(self) -> dict[str, Any]: "actual_digital_gain": metadata.get("DigitalGain"), "auto_exposure": self.auto_exposure, "auto_exposure_max_us": self.auto_exposure_max_us, + "tuning_source": self._tuning_source, + "tuning_loaded": self._tuning_loaded, + "tuning_error": self._tuning_error, "ae_flicker_mode": self.ae_flicker_mode, "noise_reduction_mode": self.noise_reduction_mode, "noise_reduction": {"off": 0, "fast": 1, "high_quality": 2}.get( @@ -1424,7 +1546,9 @@ def set_auto_exposure_max_us(self, value: int) -> bool: if not self.is_initialized: logger.error("相机未初始化") return False - self.auto_exposure_max_us = max(10_000, min(600_000, int(value))) + self.auto_exposure_max_us = max( + 10_000, min(self.AUTO_EXPOSURE_MAX_US, int(value)) + ) self._apply_frame_duration_controls() return True diff --git a/ogscope/platform/hardware/tuning/imx327.json b/ogscope/platform/hardware/tuning/imx327.json new file mode 100644 index 0000000..480e36a --- /dev/null +++ b/ogscope/platform/hardware/tuning/imx327.json @@ -0,0 +1,215 @@ +{ + "version": 2.0, + "target": "bcm2835", + "description": "OGScope 产品自维护 IMX327 tuning,长曝光曲线扩展至 1 秒 / OGScope product-owned IMX327 tuning with a one-second long-exposure curve.", + "algorithms": [ + { + "rpi.black_level": + { + "black_level": 3840 + } + }, + { + "rpi.dpc": { } + }, + { + "rpi.lux": + { + "reference_shutter_speed": 6813, + "reference_gain": 1.0, + "reference_aperture": 1.0, + "reference_lux": 890, + "reference_Y": 12900 + } + }, + { + "rpi.noise": + { + "reference_constant": 0, + "reference_slope": 2.67 + } + }, + { + "rpi.geq": + { + "offset": 187, + "slope": 0.00842 + } + }, + { + "rpi.sdn": { } + }, + { + "rpi.awb": + { + "bayes": 0 + } + }, + { + "rpi.agc": + { + "speed": 0.2, + "metering_modes": + { + "matrix": + { + "weights": + [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ] + }, + "centre-weighted": + { + "weights": + [ + 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 + ] + }, + "spot": + { + "weights": + [ + 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + } + }, + "exposure_modes": + { + "normal": + { + "shutter": [ 10, 30000, 60000 ], + "gain": [ 1.0, 2.0, 8.0 ] + }, + "short": + { + "shutter": [ 10, 5000, 10000, 20000, 120000 ], + "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ] + }, + "long": + { + "shutter": [ 1000, 30000, 60000, 120000, 250000, 500000, 1000000, 1000000, 1000000, 1000000 ], + "gain": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 4.0, 12.0 ] + } + }, + "constraint_modes": + { + "normal": [ ], + "highlight": [ + { + "bound": "LOWER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": + [ + 0, 0.5, + 1000, 0.5 + ] + }, + { + "bound": "UPPER", + "q_lo": 0.98, + "q_hi": 1.0, + "y_target": + [ + 0, 0.8, + 1000, 0.8 + ] + } + ] + }, + "y_target": + [ + 0, 0.16, + 1000, 0.16, + 10000, 0.16 + ] + } + }, + { + "rpi.alsc": + { + "omega": 1.3, + "n_iter": 100, + "luminance_strength": 0.7, + "luminance_lut": + [ + 2.844, 2.349, 2.018, 1.775, 1.599, 1.466, 1.371, 1.321, 1.306, 1.316, 1.357, 1.439, 1.552, 1.705, 1.915, 2.221, + 2.576, 2.151, 1.851, 1.639, 1.478, 1.358, 1.272, 1.231, 1.218, 1.226, 1.262, 1.335, 1.438, 1.571, 1.766, 2.067, + 2.381, 2.005, 1.739, 1.545, 1.389, 1.278, 1.204, 1.166, 1.153, 1.161, 1.194, 1.263, 1.356, 1.489, 1.671, 1.943, + 2.242, 1.899, 1.658, 1.481, 1.329, 1.225, 1.156, 1.113, 1.096, 1.107, 1.143, 1.201, 1.289, 1.423, 1.607, 1.861, + 2.152, 1.831, 1.602, 1.436, 1.291, 1.193, 1.121, 1.069, 1.047, 1.062, 1.107, 1.166, 1.249, 1.384, 1.562, 1.801, + 2.104, 1.795, 1.572, 1.407, 1.269, 1.174, 1.099, 1.041, 1.008, 1.029, 1.083, 1.146, 1.232, 1.364, 1.547, 1.766, + 2.104, 1.796, 1.572, 1.403, 1.264, 1.171, 1.097, 1.036, 1.001, 1.025, 1.077, 1.142, 1.231, 1.363, 1.549, 1.766, + 2.148, 1.827, 1.594, 1.413, 1.276, 1.184, 1.114, 1.062, 1.033, 1.049, 1.092, 1.153, 1.242, 1.383, 1.577, 1.795, + 2.211, 1.881, 1.636, 1.455, 1.309, 1.214, 1.149, 1.104, 1.081, 1.089, 1.125, 1.184, 1.273, 1.423, 1.622, 1.846, + 2.319, 1.958, 1.698, 1.516, 1.362, 1.262, 1.203, 1.156, 1.137, 1.142, 1.171, 1.229, 1.331, 1.484, 1.682, 1.933, + 2.459, 2.072, 1.789, 1.594, 1.441, 1.331, 1.261, 1.219, 1.199, 1.205, 1.232, 1.301, 1.414, 1.571, 1.773, 2.052, + 2.645, 2.206, 1.928, 1.728, 1.559, 1.451, 1.352, 1.301, 1.282, 1.289, 1.319, 1.395, 1.519, 1.685, 1.904, 2.227 + ], + "sigma": 0.005, + "sigma_Cb": 0.005 + } + }, + { + "rpi.contrast": + { + "ce_enable": 1, + "gamma_curve": + [ + 0, 0, + 1024, 5040, + 2048, 9338, + 3072, 12356, + 4096, 15312, + 5120, 18051, + 6144, 20790, + 7168, 23193, + 8192, 25744, + 9216, 27942, + 10240, 30035, + 11264, 32005, + 12288, 33975, + 13312, 35815, + 14336, 37600, + 15360, 39168, + 16384, 40642, + 18432, 43379, + 20480, 45749, + 22528, 47753, + 24576, 49621, + 26624, 51253, + 28672, 52698, + 30720, 53796, + 32768, 54876, + 36864, 57012, + 40960, 58656, + 45056, 59954, + 49152, 61183, + 53248, 62355, + 57344, 63419, + 61440, 64476, + 65535, 65535 + ] + } + }, + { + "rpi.sharpen": { } + }, + { + "rpi.ccm": + { + "ccms": [ + { + "ct": 3900, + "ccm": + [ + 1.54659, -0.17707, -0.36953, + -0.51471, 1.72733, -0.21262, + 0.06667, -0.92279, 1.85612 + ] + } + ] + } + } + ] +} diff --git a/ogscope/web/camera_shared.py b/ogscope/web/camera_shared.py index 9ae8e6e..8ecabe5 100644 --- a/ogscope/web/camera_shared.py +++ b/ogscope/web/camera_shared.py @@ -117,8 +117,13 @@ def _build_base_config(self) -> dict[str, Any]: "ae_polar_preset": settings.camera_ae_polar_preset, "ae_exposure_value": settings.camera_ae_exposure_value, "ae_aggressive_enabled": settings.camera_ae_aggressive_enabled, + "tuning_file": ( + str(settings.camera_tuning_file) + if getattr(settings, "camera_tuning_file", None) + else None + ), "auto_exposure_max_us": getattr( - settings, "camera_auto_exposure_max_us", 600_000 + settings, "camera_auto_exposure_max_us", 1_000_000 ), "capture_timeout_sec": self._capture_timeout_sec, "ae_flicker_mode": getattr(settings, "camera_ae_flicker_mode", "off"), diff --git a/pyproject.toml b/pyproject.toml index 54914b0..18eabb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ repository = "https://github.com/your-username/OGScope" documentation = "https://github.com/your-username/OGScope/docs" keywords = ["astronomy", "polar-alignment", "astrophotography", "raspberry-pi", "mipi-camera"] packages = [{include = "ogscope"}] +include = ["ogscope/platform/hardware/tuning/*.json"] [tool.poetry.dependencies] python = "^3.10" diff --git a/tests/unit/test_camera_flip.py b/tests/unit/test_camera_flip.py index 2c0e9ed..fb3925d 100644 --- a/tests/unit/test_camera_flip.py +++ b/tests/unit/test_camera_flip.py @@ -268,20 +268,20 @@ def test_preview_encoder_falls_back_to_opencv_when_turbojpeg_missing( @pytest.mark.unit def test_frame_duration_limits_allow_long_auto_exposure() -> None: cam = IMX327MIPICamera( - _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=600_000) + _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=1_000_000) ) - assert cam._compute_frame_duration_limits() == (125_000, 600_000) + assert cam._compute_frame_duration_limits() == (125_000, 1_000_000) @pytest.mark.unit -def test_auto_exposure_ceiling_is_capped_at_six_tenths_second() -> None: +def test_auto_exposure_ceiling_is_capped_at_one_second() -> None: cam = IMX327MIPICamera( _minimal_config(fps=8, auto_exposure=True, auto_exposure_max_us=2_000_000) ) - assert cam.auto_exposure_max_us == 600_000 - assert cam._compute_frame_duration_limits() == (125_000, 600_000) + assert cam.auto_exposure_max_us == 1_000_000 + assert cam._compute_frame_duration_limits() == (125_000, 1_000_000) @pytest.mark.unit diff --git a/tests/unit/test_camera_tuning.py b/tests/unit/test_camera_tuning.py new file mode 100644 index 0000000..f7b0807 --- /dev/null +++ b/tests/unit/test_camera_tuning.py @@ -0,0 +1,145 @@ +"""IMX327 产品 tuning 与曝光诊断测试 / IMX327 product tuning and exposure diagnostics tests.""" + +from __future__ import annotations + +import json +from typing import Any + +import numpy as np +import pytest + +from ogscope.platform.hardware.camera import IMX327MIPICamera + + +def _camera(**extra: Any) -> IMX327MIPICamera: + config: dict[str, Any] = { + "width": 1280, + "height": 720, + "fps": 8, + "auto_exposure": True, + } + config.update(extra) + return IMX327MIPICamera(config) + + +@pytest.mark.unit +def test_product_tuning_long_curve_reaches_one_second() -> None: + """产品曲线优先延长曝光并最终到达 1 秒 / Product curve prioritizes shutter and reaches 1s.""" + with IMX327MIPICamera.PRODUCT_TUNING_FILE.open(encoding="utf-8") as file: + tuning = json.load(file) + + agc = next(item["rpi.agc"] for item in tuning["algorithms"] if "rpi.agc" in item) + long_mode = agc["exposure_modes"]["long"] + + assert long_mode["shutter"][-1] == 1_000_000 + assert len(long_mode["shutter"]) == len(long_mode["gain"]) + first_max_shutter = long_mode["shutter"].index(1_000_000) + assert long_mode["gain"][: first_max_shutter + 1] == [1.0] * (first_max_shutter + 1) + assert long_mode["gain"][-1] == 12.0 + + +@pytest.mark.unit +def test_product_tuning_loader_reports_bundled_source() -> None: + """产品 tuning 成功时记录可诊断来源 / Report the bundled source after a successful load.""" + + class FakePicamera2: + @staticmethod + def load_tuning_file(name: str, *, dir: str) -> dict[str, str]: + assert name == "imx327.json" + assert dir.endswith("hardware/tuning") + return {"loaded": name} + + camera = _camera() + tuning = camera._load_picamera_tuning(FakePicamera2) + + assert tuning == {"loaded": "imx327.json"} + assert camera._tuning_source == "product:imx327.json" + assert camera._tuning_loaded is True + assert camera._tuning_error is None + + +@pytest.mark.unit +def test_tuning_loader_falls_back_without_exposing_override_path(tmp_path) -> None: + """覆盖文件失败时安全回退且 API 状态不泄露路径 / Fall back without exposing override paths.""" + + class MissingTuningPicamera2: + @staticmethod + def load_tuning_file(name: str, *, dir: str) -> dict[str, str]: + raise FileNotFoundError(f"{dir}/{name}") + + secret_path = tmp_path / "private-camera-tuning.json" + camera = _camera(tuning_file=str(secret_path)) + + assert camera._load_picamera_tuning(MissingTuningPicamera2) is None + assert camera._tuning_source == "system_default" + assert camera._tuning_loaded is False + assert camera._tuning_error == "FileNotFoundError" + assert str(tmp_path) not in camera._tuning_error + + +@pytest.mark.unit +def test_histogram_stats_use_lores_and_include_highlight_tail() -> None: + """亮度统计使用有界直方图并保留高光尾部 / Use bounded histograms and retain highlight-tail data.""" + + class Request: + @staticmethod + def make_array(stream: str) -> np.ndarray: + assert stream == "lores" + return np.array([[0, 10, 20, 30], [40, 50, 250, 255]], dtype=np.uint8) + + camera = _camera(lores_width=4, lores_height=2) + camera._lores_available = True + camera._collect_lores_stats(Request()) + + stats = camera._last_lores_stats + assert stats["source"] == "lores" + assert stats["sample_count"] == 8 + assert stats["p50"] == 30.0 + assert stats["p99_8"] == 255.0 + assert stats["saturated_fraction"] == pytest.approx(0.25) + + +@pytest.mark.unit +def test_histogram_stats_fall_back_to_bounded_main_stream() -> None: + """无 lores 时仅有界采样主流绿通道 / Sample a bounded main-stream green channel without lores.""" + camera = _camera() + camera._lores_available = False + image = np.zeros((720, 1280, 3), dtype=np.uint8) + image[..., 1] = 64 + + camera._collect_lores_stats(object(), main_image=image) + + stats = camera._last_lores_stats + assert stats["source"] == "main_fallback" + assert stats["sample_count"] <= camera.LUMINANCE_STATS_MAX_SAMPLES + assert stats["p50"] == 64.0 + + +@pytest.mark.unit +def test_frame_duration_control_reports_primary_and_fallback_paths() -> None: + """帧周期控制必须明确报告主路径和降级路径 / Report primary and fallback frame-duration paths.""" + + class FakeCamera: + camera_controls = {"FrameDurationLimits": object()} + + def __init__(self) -> None: + self.fail_limits = False + self.controls: list[dict[str, Any]] = [] + + def set_controls(self, controls: dict[str, Any]) -> None: + if self.fail_limits and "FrameDurationLimits" in controls: + raise RuntimeError("unsupported at runtime") + self.controls.append(controls) + + camera = _camera() + fake = FakeCamera() + camera.camera = fake + + camera._apply_frame_duration_controls() + assert camera._frame_duration_control == "frame_duration_limits" + + fake.fail_limits = True + camera._apply_frame_duration_controls() + assert camera._frame_duration_control == "frame_rate_fallback" + assert fake.controls[-1] == {"FrameRate": 8.0} + assert camera._frame_duration_control_error == "RuntimeError" diff --git a/tests/unit/test_config_catalog.py b/tests/unit/test_config_catalog.py index ac7844e..d48c289 100644 --- a/tests/unit/test_config_catalog.py +++ b/tests/unit/test_config_catalog.py @@ -17,6 +17,7 @@ def test_build_config_catalog_includes_new_preview_fields() -> None: assert "OGSCOPE_SHARED_PREVIEW_FPS" in keys assert "OGSCOPE_PREVIEW_JPEG_QUALITY" in keys assert "OGSCOPE_PREVIEW_ENCODER" in keys + assert "OGSCOPE_CAMERA_TUNING_FILE" in keys assert "OGSCOPE_CAMERA_AUTO_EXPOSURE_MAX_US" in keys assert "OGSCOPE_CAMERA_NOISE_REDUCTION_MODE" in keys assert "OGSCOPE_SIMULATION_MODE" in keys @@ -41,8 +42,8 @@ def test_simulation_mode_tri_state(raw: str | None, expected: bool | None) -> No @pytest.mark.unit -def test_legacy_auto_exposure_ceiling_is_capped_at_six_tenths_second() -> None: - """旧环境值不阻止启动且会收敛到 0.6 秒 / Legacy values boot and clamp to 0.6s.""" +def test_legacy_auto_exposure_ceiling_is_capped_at_one_second() -> None: + """旧环境值不阻止启动且会收敛到 1 秒 / Legacy values boot and clamp to 1s.""" settings = Settings(camera_auto_exposure_max_us=2_000_000) - assert settings.camera_auto_exposure_max_us == 600_000 + assert settings.camera_auto_exposure_max_us == 1_000_000 diff --git a/web/spa/src/apps/camera/CameraConsoleApp.tsx b/web/spa/src/apps/camera/CameraConsoleApp.tsx index 142bc81..863afb3 100644 --- a/web/spa/src/apps/camera/CameraConsoleApp.tsx +++ b/web/spa/src/apps/camera/CameraConsoleApp.tsx @@ -61,6 +61,11 @@ type CameraInfo = { colour_temperature?: number; lux?: number; frame_duration_limits?: number[]; + frame_duration_control?: string; + frame_duration_control_error?: string | null; + tuning_source?: string; + tuning_loaded?: boolean; + tuning_error?: string | null; lores_enabled?: boolean; lores_available?: boolean; lores_width?: number; @@ -241,7 +246,7 @@ const RES_PRESETS = ["640x360", "1280x720", "1600x900", "1920x1020"] as const; const ROTATION_PRESETS = [0, 90, 180, 270] as const; const FILE_PAGE_SIZE = 12; const MANUAL_EXPOSURE_MIN_SECONDS = 0.0001; -const MANUAL_EXPOSURE_MAX_SECONDS = 0.6; +const MANUAL_EXPOSURE_MAX_SECONDS = 1.0; const MANUAL_EXPOSURE_STEP_SECONDS = 0.0001; function clamp(v: number, min: number, max: number): number { @@ -430,7 +435,7 @@ function ExposureControl({ disabled={disabled} aria-label={label} onChange={(e) => { - // 对数滑块兼顾短曝光精调与 0.6 秒长曝光 / Log scale preserves short-exposure precision up to 0.6 s. + // 对数滑块兼顾短曝光精调与 1 秒长曝光 / Log scale preserves short-exposure precision up to 1 s. const next = Number((10 ** Number(e.target.value)).toFixed(6)); onChange(clamp(next, MANUAL_EXPOSURE_MIN_SECONDS, MANUAL_EXPOSURE_MAX_SECONDS)); }} @@ -493,7 +498,7 @@ export function CameraConsoleApp() { noiseReduction: 0, noiseReductionMode: "fast", aeFlickerMode: "off", - autoExposureMaxSeconds: 0.6, + autoExposureMaxSeconds: 1.0, whiteBalanceMode: "auto", whiteBalanceGainR: 1.0, whiteBalanceGainB: 1.0, @@ -847,7 +852,7 @@ export function CameraConsoleApp() { noiseReduction: clamp(Math.round(toNum(info.noise_reduction, 0)), 0, 4), noiseReductionMode: String(info.noise_reduction_mode ?? "fast"), aeFlickerMode: String(info.ae_flicker_mode ?? "off"), - autoExposureMaxSeconds: clamp(exposureUsToSeconds(info.auto_exposure_max_us, 600000), 0.01, 0.6), + autoExposureMaxSeconds: clamp(exposureUsToSeconds(info.auto_exposure_max_us, 1_000_000), 0.01, 1.0), whiteBalanceMode: String(info.white_balance_mode ?? "auto"), whiteBalanceGainR: clamp(toNum(info.white_balance_gain_r, 1.0), 0.1, 3.0), whiteBalanceGainB: clamp(toNum(info.white_balance_gain_b, 1.0), 0.1, 3.0), @@ -2028,6 +2033,12 @@ export function CameraConsoleApp() {

{`${status?.info?.backend ?? streamMetrics?.camera_backend ?? "—"} · lores ${status?.info?.lores_available || streamMetrics?.lores_available ? "on" : "off"}`}

+

+ {`tuning ${status?.info?.tuning_source ?? "—"} · frame ${status?.info?.frame_duration_control ?? "—"}`} + {status?.info?.tuning_error || status?.info?.frame_duration_control_error + ? ` · fallback ${status.info.tuning_error ?? status.info.frame_duration_control_error}` + : ""} +

{t("cam.stats.metadata")}: @@ -2295,7 +2306,7 @@ export function CameraConsoleApp() { { setForm((p) => ({ ...p, whiteBalanceGainR: Number(v.toFixed(1)) })); setFormDirty(true); }} /> { setForm((p) => ({ ...p, whiteBalanceGainB: Number(v.toFixed(1)) })); setFormDirty(true); }} /> diff --git a/web/static/analysis-lab/assets/camera-C3Lweuht.js b/web/static/analysis-lab/assets/camera-C3Lweuht.js new file mode 100644 index 0000000..c2d91d3 --- /dev/null +++ b/web/static/analysis-lab/assets/camera-C3Lweuht.js @@ -0,0 +1,61 @@ +import{r as o,j as e,a as Fr,R as Er}from"./client-D1ZVDB-N.js";import{c as T,u as Rr,T as $r,I as Br}from"./index-BoSUzpvv.js";import{u as Pr,S as Mr,C as Ia,X as Tr,r as Ir,b as Ar}from"./http-CaEmWEuL.js";import{R as Or}from"./rotate-ccw-D6okH6-S.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 Aa=T("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** + * @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 yt=T("Crosshair",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]]);/** + * @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 Hr=T("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @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 Dr=T("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** + * @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 Gr=T("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** + * @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 zr=T("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** + * @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 Lr=T("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @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 Oa=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** + * @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 qr=T("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** + * @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 Ur=T("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** + * @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 Wr=T("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** + * @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 Vr=T("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),Xr="/api/dev/debug";function H(a){return`${Xr}${a}`}async function g(a,b={}){const f=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Ir(f,b)}const Jr=["640x360","1280x720","1600x900","1920x1020"],Kr=[0,90,180,270],Je=12,ce=1e-4,le=1,Yr=1e-4;function N(a,b,f){return Math.min(f,Math.max(b,a))}function k(a,b){return a==null||Number.isNaN(Number(a))?b:Number(a)}function de(a,b){return k(a,b)/1e6}function wt(a){return Math.round(a*1e6)}function jt(a){return`${de(a,0).toFixed(4)} s`}function Ha(a){const{exposureSeconds:b,autoExposureMaxSeconds:f,...p}=a;return{...p,exposure:wt(b),autoExposureMaxUs:wt(f)}}function Da(a){if(!a)return"0 B";const b=["B","KB","MB","GB"];let f=0,p=a;for(;p>=1024&&fE.hfd),p=Math.min(...f),r=Math.max(...f),J=Math.max(.2,r-p),m=f.map((E,K)=>{const ue=K/Math.max(1,f.length-1)*320,l=76-(E-p)/J*60;return`${ue.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:m,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((B=m.split(" ").at(-1))==null?void 0:B.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Ga(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const b=await a.json(),f=Number(b.max_clients??0),p=Number(b.active_clients??0);return f>0&&p>=f?"busy":"ok"}catch{return"fail"}}function X({label:a,value:b,min:f,max:p,step:r,onChange:J,disabled:m=!1,unit:B=""}){const E=r>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(r))));return e.jsxs("label",{className:`block ${m?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[b.toFixed(E),B]})]}),e.jsx("input",{type:"range",min:f,max:p,step:r,value:b,disabled:m,onChange:K=>J(Number(K.target.value)),className:"w-full accent-primary"})]})}function Qr({label:a,value:b,disabled:f,onChange:p}){const r=N(b,ce,le),J=Math.log10(ce),m=Math.log10(le);return e.jsxs("div",{className:`block ${f?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between gap-2",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("input",{type:"number",min:ce,max:le,step:Yr,value:Number(r.toFixed(6)),disabled:f,"aria-label":a,onChange:B=>{const E=Number(B.target.value);Number.isFinite(E)&&p(N(E,ce,le))},className:"w-24 rounded border border-outline-variant/30 bg-surface-container-low px-1.5 py-0.5 text-right font-mono text-[11px]"}),e.jsx("span",{className:"font-mono text-[11px]",children:"s"})]})]}),e.jsx("input",{type:"range",min:J,max:m,step:.001,value:Math.log10(r),disabled:f,"aria-label":a,onChange:B=>{const E=Number((10**Number(B.target.value)).toFixed(6));p(N(E,ce,le))},className:"w-full accent-primary"})]})}function en(){var ua,ma,xa,pa,ha,fa,ga,ba,va,ya,ja,wa,Na,Sa,_a,ka,Ca,Fa,Ea,Ra,$a,Ba,Pa,Ma;const{t:a,locale:b,setLocale:f}=Rr(),{info:p}=Pr(),[r,J]=o.useState(null),[m,B]=o.useState(!1),[E,K]=o.useState(()=>Date.now()),[ue,l]=o.useState(null),[we,Q]=o.useState(null),[Nt,ee]=o.useState(!1),[me,v]=o.useState(null),[I,Ne]=o.useState(!1),[Ke,St]=o.useState(!1),[_t,kt]=o.useState(!1),[Ye,Ct]=o.useState("8"),[Ft,Et]=o.useState("8"),[Rt,$t]=o.useState("1280x720"),[Bt,Pt]=o.useState("supersample"),[xe,te]=o.useState(!1),[Mt,za]=o.useState(!0),[Ze,La]=o.useState(!0),[Qe,qa]=o.useState(!1),[Tt,Ua]=o.useState(!1),[It,At]=o.useState(!1),[et,Wa]=o.useState({mean:0,std:0,over:0}),[Va,Se]=o.useState(0),[i,tt]=o.useState(null),[_,at]=o.useState(!1),[ae,_e]=o.useState(!1),[s,Ot]=o.useState(null),[ke,pe]=o.useState([]),[re,he]=o.useState(null),[j,rt]=o.useState(null),[Ht,D]=o.useState(null),[Dt,Gt]=o.useState(0),[zt,Lt]=o.useState(180),[Ce,qt]=o.useState(!1),[Fe,Ut]=o.useState(!1),[d,S]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:1,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ee,w]=o.useState(!1),[nt,Wt]=o.useState(""),[Vt,Xt]=o.useState(""),[Jt,Xa]=o.useState([]),[st,L]=o.useState(!1),[fe,Ja]=o.useState([]),[Kt,Yt]=o.useState(!1),[C,ot]=o.useState(null),[Ka,it]=o.useState(!1),[Re,ct]=o.useState(null),[lt,$e]=o.useState(1),P=o.useRef(null),Be=o.useRef(null),ge=o.useRef(null),Pe=o.useRef(null),Me=o.useRef(null),q=o.useRef(null),Te=o.useRef(null),dt=o.useRef(!1),ne=o.useRef(!1),Y=o.useRef(!1),Ie=o.useRef(null),U=o.useRef([]),se=o.useRef(0),ut=o.useRef(null),Zt=()=>{U.current=[],se.current=0,Ie.current=null,Se(0)},R=async()=>{try{const t=await g("/api/debug/camera/status",{cache:"no-store"});J(t),t.streaming||(B(!1),Y.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},be=()=>{Te.current!=null&&(window.clearTimeout(Te.current),Te.current=null)},mt=async()=>{if(I)return!1;Ne(!0),l(null),Q(null),ee(!1);try{be(),r!=null&&r.streaming||await g("/api/debug/camera/start",{method:"POST"});const t=Date.now(),n=await Ga();return n==="busy"?(Q(a("cam.err.streamBusy")),ee(!0),!1):n==="fail"?(Q(a("cam.err.streamProbeFailed")),ee(!1),!1):(B(!0),Y.current=!0,v(a("cam.notice.previewStart")),Zt(),Ie.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{Ne(!1)}},Qt=async()=>{if(!ae){_e(!0),D(null);try{if(!(m||await mt()))return;const n=await g(H("/camera/focus/session/start"),{method:"POST"});ne.current=n.active,S(c=>({...c,autoExposure:!1,exposureSeconds:de(n.locked.exposure,5e3),gain:n.locked.gain,digitalGain:n.locked.digitalGain,noiseReduction:0,noiseReductionMode:n.locked.noiseReductionMode})),w(!1),Ot(null),pe([]),he(null),rt(null),At(!0),at(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){D(t instanceof Error?t.message:String(t))}finally{_e(!1)}}},xt=async()=>{if(!ae){_e(!0),at(!1),D(null);try{if(ne.current){const t=await g(H("/camera/focus/session/stop"),{method:"POST"});ne.current=!1,t.restored_settings&&(S(n=>({...n,autoExposure:t.restored_settings.autoExposure,exposureSeconds:de(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){D(t instanceof Error?t.message:String(t))}finally{_e(!1)}}},Ya=()=>{pe([]),he(null),D(null)},Za=t=>{if(!_||!P.current||t.target.closest("button, label, input"))return;const n=P.current,c=t.currentTarget.getBoundingClientRect(),h=n.naturalWidth||(s==null?void 0:s.frame.width)||0,u=n.naturalHeight||(s==null?void 0:s.frame.height)||0;if(h<=0||u<=0)return;const x=Math.min(c.width/h,c.height/u),F=h*x,y=u*x,$=(c.width-F)/2,M=(c.height-y)/2,z=(t.clientX-c.left-$)/F,oe=(t.clientY-c.top-M)/y;z<0||z>1||oe<0||oe>1||(rt({x:z,y:oe}),pe([]),he(null),D(null))},Qa=()=>{rt(null),pe([]),he(null),D(null)},er=async()=>{if(!I){Ne(!0),l(null);try{be(),B(!1),Y.current=!1,Q(null),ee(!1),Zt(),P.current&&(P.current.onload=null,P.current.onerror=null,P.current.src="",P.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},tr=async()=>{if(!(!m&&!(r!=null&&r.streaming))){kt(!0),l(null);try{const t=await g("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{kt(!1)}}},ar=async()=>{if(!Ke){St(!0),l(null);try{if(r!=null&&r.recording)await g("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await g("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{St(!1)}}},rr=async()=>{l(null);try{const t=N(parseInt(Ye,10)||5,1,60);await g(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const n=N(parseInt(Ft,10)||8,1,30);await g(`/api/debug/camera/preview-fps?fps=${n}`,{method:"POST"});const[c,h]=Rt.split("x").map(u=>parseInt(u,10));c&&h&&await g(`/api/debug/camera/size?width=${c}&height=${h}`,{method:"POST"}),await g(`/api/debug/camera/sampling?mode=${encodeURIComponent(Bt)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),te(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},nr=async()=>{l(null);try{await g("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ha(d))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},sr=async()=>{l(null);try{await g("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ha(d))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},or=(t,n)=>{if(!t)return;const c=(n==null?void 0:n.syncRuntime)??!0;S({exposureSeconds:N(de(t.exposure_us,5e3),ce,le),gain:N(k(t.analogue_gain,1),1,24),digitalGain:N(k(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:N(k(t.contrast,1),0,2),brightness:N(k(t.brightness,0),-1,1),saturation:N(k(t.saturation,1),0,2),sharpness:N(k(t.sharpness,1),0,2),noiseReduction:N(Math.round(k(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:N(de(t.auto_exposure_max_us,1e6),.01,1),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:N(k(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:N(k(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(Ct(String(Math.round(k(t.fps,8)))),$t(`${Math.round(k(t.width,1280))}x${Math.round(k(t.height,720))}`),Pt(String(t.sampling_mode??"supersample")),te(!1)),Lt(N(Math.round(k(t.rotation,180)),0,270)),qt(!!t.flip_horizontal),Ut(!!t.flip_vertical),w(!1)},ea=async(t,n)=>{l(null);try{await g("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:n})}),qt(t),Ut(n),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},ir=async t=>{l(null);try{await g(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Lt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(n){l(n instanceof Error?n.message:String(n))}},ta=async t=>{l(null);try{await g(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(n){l(n instanceof Error?n.message:String(n))}},cr=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await g("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},lr=async()=>{l(null);try{await g("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},dr=async()=>{l(null);try{await g("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ae=async()=>{L(!0);try{const t=await g("/api/debug/camera/presets",{cache:"no-store"});Xa(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{L(!1)}},ur=async()=>{const t=nt.trim();if(t){L(!0),l(null);try{await g("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Vt.trim(),exposure_us:wt(d.exposureSeconds),analogue_gain:d.gain,digital_gain:d.digitalGain,auto_exposure:d.autoExposure,contrast:d.contrast,brightness:d.brightness,saturation:d.saturation,sharpness:d.sharpness,noise_reduction:d.noiseReduction,white_balance_mode:d.whiteBalanceMode,white_balance_gain_r:d.whiteBalanceGainR,white_balance_gain_b:d.whiteBalanceGainB,rotation:zt,flip_horizontal:Ce,flip_vertical:Fe,color_mode:d.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Xt(""),await Ae()}catch(n){l(n instanceof Error?n.message:String(n))}finally{L(!1)}}},mr=async t=>{L(!0),l(null);try{await g(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Ae()}catch(n){l(n instanceof Error?n.message:String(n))}finally{L(!1)}},xr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){L(!0),l(null);try{await g(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Ae()}catch(n){l(n instanceof Error?n.message:String(n))}finally{L(!1)}}},pt=async()=>{Yt(!0);try{const t=await g("/api/debug/files",{cache:"no-store"});Ja(t.files??[]),$e(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Yt(!1)}},ht=()=>{ct(null),ot(null),it(!1)},pr=async t=>{if(Re===t){ht();return}ct(t),ot(null),it(!0),l(null);try{const n=await g(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});ot(n)}catch(n){l(n instanceof Error?n.message:String(n)),ct(null)}finally{it(!1)}},hr=t=>{const n=(h,u)=>{const x=document.createElement("a");x.href=u,x.download=h,document.body.appendChild(x),x.click(),document.body.removeChild(x)};n(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const u=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(u)}`)).ok)return;n(u,`${H("/files")}/${encodeURIComponent(u)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:u}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},fr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await g(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Re===t||(C==null?void 0:C.filename)===t)&&ht(),await pt()}catch(n){l(n instanceof Error?n.message:String(n))}}},gr=()=>{if(!Mt||!P.current||!Be.current)return;const t=P.current;if(!t.naturalWidth||!t.naturalHeight||(ge.current||(ge.current=document.createElement("canvas"),Pe.current=ge.current.getContext("2d",{willReadFrequently:!0})),Me.current||(Me.current=Be.current.getContext("2d")),!Pe.current||!Me.current))return;const c=Math.min(1,320/t.naturalWidth),h=Math.max(1,Math.round(t.naturalWidth*c)),u=Math.max(1,Math.round(t.naturalHeight*c));ge.current.width=h,ge.current.height=u,Pe.current.drawImage(t,0,0,h,u);const x=Pe.current.getImageData(0,0,h,u).data,F=new Array(256).fill(0),y=new Array(256).fill(0),$=new Array(256).fill(0),M=new Array(256).fill(0);let z=0,oe=0,Ta=0;const ie=h*u;for(let O=0;O=250&&(Ta+=1)}const vt=ie?z/ie:0,kr=ie?oe/ie-vt*vt:0;Wa({mean:vt,std:Math.sqrt(Math.max(0,kr)),over:ie?Ta/ie*100:0});const Ue=Be.current,A=Me.current,We=window.devicePixelRatio||1,ve=Math.max(1,Ue.clientWidth||240),ye=Math.max(1,Ue.clientHeight||120);Ue.width=Math.floor(ve*We),Ue.height=Math.floor(ye*We),A.setTransform(We,0,0,We,0,0),A.clearRect(0,0,ve,ye);const Cr=Math.max(1,...Ze?[Math.max(...F),Math.max(...y),Math.max(...$)]:[0],...Qe?[Math.max(...M)]:[0]),Ve=(O,Xe)=>{A.beginPath(),A.strokeStyle=Xe,A.lineWidth=1.2;for(let W=0;W<256;W+=1){const je=W/255*ve,V=ye-O[W]/Cr*ye;W===0?A.moveTo(je,V):A.lineTo(je,V)}A.stroke()};if(Ze&&(Ve(F,"rgba(255,80,80,0.85)"),Ve(y,"rgba(80,255,80,0.85)"),Ve($,"rgba(80,160,255,0.85)")),Qe&&Ve(M,"rgba(255,255,255,0.95)"),Tt){const O=.9803921568627451*ve;A.fillStyle="rgba(255,100,100,0.12)",A.fillRect(O,0,ve-O,ye)}};o.useEffect(()=>{R(),Ae(),pt();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[r==null?void 0:r.streaming]),o.useEffect(()=>{!(r!=null&&r.info)||Ee||or(r.info,{syncRuntime:!xe})},[r==null?void 0:r.info,Ee,xe]),o.useEffect(()=>{if(!(r!=null&&r.recording)){q.current&&(window.clearInterval(q.current),q.current=null),Gt(0);return}if(q.current)return;const t=Date.now();return q.current=window.setInterval(()=>{Gt(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{q.current&&(window.clearInterval(q.current),q.current=null)}},[r==null?void 0:r.recording]),o.useEffect(()=>{Y.current=m,m||(be(),at(!1),ne.current&&xt())},[m]),o.useEffect(()=>()=>{ne.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),ne.current=!1)},[]),o.useEffect(()=>{if(!_||!m)return;let t=!1;const n=async()=>{var h,u;if(!(dt.current||document.hidden)){dt.current=!0;try{const x=new URLSearchParams;j&&(x.set("target_x",j.x.toFixed(6)),x.set("target_y",j.y.toFixed(6)));const F=x.size>0?`?${x.toString()}`:"",y=await g(`${H("/camera/focus/metrics")}${F}`,{cache:"no-store"});if(t)return;Ot(y),D(null);const $=j?(h=y.selected_star)==null?void 0:h.hfd_px:(u=y.aggregate)==null?void 0:u.median_hfd_px;typeof $=="number"&&Number.isFinite($)&&(pe(M=>{var z;return((z=M.at(-1))==null?void 0:z.frameId)===y.frame_id?M:[...M,{frameId:y.frame_id,hfd:$}].slice(-60)}),he(M=>M==null?$:Math.min(M,$)))}catch(x){t||D(x instanceof Error?x.message:String(x))}finally{dt.current=!1}}};n();const c=window.setInterval(n,850);return()=>{t=!0,window.clearInterval(c)}},[_,m,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{m&&(U.current=[],se.current=0)},[E,m]),o.useEffect(()=>{if(!m){U.current=[],se.current=0,Se(0);return}if(!ut.current){const u=document.createElement("canvas");u.width=32,u.height=32,ut.current=u}const n=ut.current.getContext("2d",{willReadFrequently:!0});if(!n)return;let c=0;const h=()=>{const u=P.current;if(u!=null&&u.complete&&u.naturalWidth>0)try{n.drawImage(u,0,0,32,32);const x=n.getImageData(0,0,32,32).data;let F=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[m]),o.useEffect(()=>{if(!m){Se(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,h=U.current;for(;h.length&&h[0]window.clearInterval(t)},[m]),o.useEffect(()=>{if(!m){tt(null);return}let t=!1;const n=async()=>{try{const h=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!h.ok||t)return;const u=await h.json();if(!t){tt(u);const x=Number(u.preview_target_fps??u.target_preview_fps??0);Number.isFinite(x)&&x>0&&!xe&&Et(String(Math.round(x)))}}catch{t||tt(null)}};n();const c=window.setInterval(n,2e3);return()=>{t=!0,window.clearInterval(c)}},[m,xe]),o.useEffect(()=>{if(!me)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[me]),o.useEffect(()=>()=>be(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(fe.length/Je));lt>t&&$e(t)},[fe.length,lt]);const br=m?`${H("/camera/stream")}?t=${E}`:"",Oe=d.autoExposure,ft=d.whiteBalanceMode==="manual",vr=N(parseInt(Ye,10)||Math.round(k((ua=r==null?void 0:r.info)==null?void 0:ua.fps,8)),1,60),aa=!d.autoExposure&&1/d.exposureSeconds0?Z.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],jr=Array.isArray(Z.noise_reduction_modes)&&Z.noise_reduction_modes.length>0?Z.noise_reduction_modes:["off","fast","high_quality"],ra=Z.manual_digital_gain!==!1,na=!!((xa=r==null?void 0:r.info)!=null&&xa.night_mode),sa=m,He=!I&&!m&&!(r!=null&&r.recording),oa=!I&&m,wr=!I&&!_t&&sa,Nr=!I&&!Ke&&sa,G=j?s==null?void 0:s.selected_star:null,De=j?(G==null?void 0:G.hfd_px)??null:((pa=s==null?void 0:s.aggregate)==null?void 0:pa.median_hfd_px)??null,ia=j?(G==null?void 0:G.fwhm_px)??null:((ha=s==null?void 0:s.aggregate)==null?void 0:ha.median_fwhm_px)??null,ca=j?(G==null?void 0:G.concentration)??null:((fa=s==null?void 0:s.aggregate)==null?void 0:fa.median_concentration)??null,Ge=De!=null&&re!=null&&re>0?(De-re)/re*100:null,la=ke.slice(-3).map(t=>t.hfd),gt=ke.slice(-6,-3).map(t=>t.hfd),bt=t=>t.reduce((n,c)=>n+c,0)/Math.max(1,t.length);let ze="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")ze="cam.focus.guidance.noStars";else if(ke.length>=5&&Ge!=null&&Ge<=3)ze="cam.focus.guidance.best";else if(la.length===3&>.length===3){const t=bt(la)-bt(gt),n=Math.max(.04,bt(gt)*.02);ze=t<-n?"cam.focus.guidance.improving":t>n?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const Sr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Le=Math.max(1,Math.ceil(fe.length/Je)),qe=Math.min(lt,Le),da=(qe-1)*Je,_r=fe.slice(da,da+Je);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${b==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(qr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Kr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void ir(t),className:`rounded border px-2 py-1 ${zt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void ea(!Ce,Fe),className:`rounded border px-2 py-1 ${Ce?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void ea(Ce,!Fe),className:`rounded border px-2 py-1 ${Fe?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void lr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Mr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void dr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Gr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void cr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ta(!0),className:`rounded border px-2 py-1 ${na?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Lr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ta(!1),className:`rounded border px-2 py-1 ${na?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Vr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:nt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Vt,onChange:t=>Xt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:st||!nt.trim(),onClick:()=>void ur(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Jt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Jt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",jt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:st,onClick:()=>void mr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:st,onClick:()=>void xr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.cpu_usage)==null?"—":`${Number(p.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.memory_usage)==null?"—":`${Number(p.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.temperature)==null?"—":`${Number(p.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:ae||I||!!(r!=null&&r.recording),onClick:()=>_?void xt():void Qt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${_?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(yt,{className:"mr-1 inline h-3.5 w-3.5"}),a(ae?"cam.focus.starting":_?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(m?"cam.state.streaming":"cam.state.idle")]})]})]}),we&&!m&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${_?"cursor-crosshair":""}`,onClick:Za,children:[m&&we&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),m?e.jsx("img",{ref:P,alt:"camera-preview",className:"h-full w-full object-contain",src:br,onLoad:()=>{Q(null),ee(!1),gr()},onError:()=>{var n;if(be(),!Y.current)return;const t=(n=P.current)==null?void 0:n.src;(async()=>{if(!Y.current||!t)return;if(await Ga()==="busy"){Q(a("cam.err.streamBusy")),ee(!0);return}Te.current=window.setTimeout(()=>{Y.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ia,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Oa,{className:"mr-1 inline h-3.5 w-3.5"}),a(I?"cam.btn.starting":"cam.btn.start")]})]}),m&&_&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,n)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${n}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(r==null?void 0:r.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Aa,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Dt/60).toString().padStart(2,"0")}:${(Dt%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>At(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(It?"cam.hist.expand":"cam.hist.collapse")})}),!It&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Mt,onChange:t=>za(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ze,onChange:t=>La(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Qe,onChange:t=>qa(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Tt,onChange:t=>Ua(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Be,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:et.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:et.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[et.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${_?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(yt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${_?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(_?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),_?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[_?e.jsx("button",{type:"button",onClick:()=>void xt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:ae||I||!!(r!=null&&r.recording),onClick:()=>void Qt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(yt,{className:"mr-1 inline h-4 w-4"}),a(ae?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!_,onClick:Ya,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!_||j==null,onClick:Qa,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),_?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:De==null?"—":De.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(ze)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:re==null?"—":`${re.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ge==null?"—":`+${Math.max(0,Ge).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(Sr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ia==null?"—":`${ia.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ca==null?"—":`${(ca*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Zr,{samples:ke,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,n)=>t+n,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??Va).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((ga=r==null?void 0:r.info)==null?void 0:ga.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((ba=r==null?void 0:r.info)==null?void 0:ba.actual_exposure_us)!=null?jt((i==null?void 0:i.actual_exposure_us)??((va=r==null?void 0:r.info)==null?void 0:va.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(ya=r==null?void 0:r.info)!=null&&ya.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:de(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Ie.current!=null?`${Math.max(0,Math.round((performance.now()-Ie.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ja=r==null?void 0:r.info)==null?void 0:ja.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((wa=r==null?void 0:r.info)==null?void 0:wa.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Na=r==null?void 0:r.info)==null?void 0:Na.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(Sa=r==null?void 0:r.info)!=null&&Sa.lores_available||i!=null&&i.lores_available?"on":"off"}`}),e.jsxs("p",{className:"mt-0.5 break-all text-[10px] leading-tight text-on-surface-variant/90",children:[`tuning ${((_a=r==null?void 0:r.info)==null?void 0:_a.tuning_source)??"—"} · frame ${((ka=r==null?void 0:r.info)==null?void 0:ka.frame_duration_control)??"—"}`,(Ca=r==null?void 0:r.info)!=null&&Ca.tuning_error||(Fa=r==null?void 0:r.info)!=null&&Fa.frame_duration_control_error?` · fallback ${r.info.tuning_error??r.info.frame_duration_control_error}`:""]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((Ea=r==null?void 0:r.info)==null?void 0:Ea.lux)!=null?`${Number(r.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((Ra=r==null?void 0:r.info)==null?void 0:Ra.colour_temperature)!=null?`${Math.round(Number(r.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${(($a=r==null?void 0:r.info)==null?void 0:$a.width)??"—"}x${((Ba=r==null?void 0:r.info)==null?void 0:Ba.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Pa=r==null?void 0:r.info)!=null&&Pa.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Oa,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!oa,onClick:()=>void er(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${oa?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Wr,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!wr,onClick:()=>void tr(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ia,{className:"inline h-4 w-4"})," ",a(_t?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!Nr,onClick:()=>void ar(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${r!=null&&r.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Aa,{className:"inline h-4 w-4"})," ",Ke?a("cam.btn.recordBusy"):r!=null&&r.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Kt,onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Ka&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),C&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Dr,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:C.filename})]}),e.jsx("button",{type:"button",onClick:()=>ht(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx(Tr,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Da(C.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:C.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(C.modified).toLocaleString()})]}),C.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:jt(C.exposure_us)})]}),C.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:C.analogue_gain})]}),C.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:C.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[fe.length===0&&!Kt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),_r.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Re===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Da(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>hr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Hr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void pr(t.name),className:`rounded border px-2 py-1 ${Re===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(zr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void fr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx($r,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:qe<=1,onClick:()=>$e(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:qe,total:Le})}),e.jsx("button",{type:"button",disabled:qe>=Le,onClick:()=>$e(t=>Math.min(Le,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:Ye,onChange:t=>{Ct(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:Ft,onChange:t=>{Et(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Bt,onChange:t=>{Pt(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Jr.map(t=>e.jsx("button",{type:"button",onClick:()=>{$t(t),te(!0)},className:`rounded border px-2 py-1 ${Rt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!xe,onClick:()=>void rr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Ur,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ee&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Oe&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(Qr,{label:a("cam.controls.exposure"),value:d.exposureSeconds,disabled:Oe,onChange:t=>{w(!0),S(n=>({...n,exposureSeconds:t}))}}),e.jsx(X,{label:a("cam.controls.gain"),value:d.gain,min:1,max:24,step:.1,disabled:Oe,onChange:t=>{w(!0),S(n=>({...n,gain:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.digitalGain"),value:d.digitalGain,min:1,max:8,step:.1,disabled:Oe||!ra,onChange:t=>{w(!0),S(n=>({...n,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:d.noiseReductionMode,onChange:t=>{w(!0),S(n=>({...n,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:jr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(X,{label:a("cam.controls.contrast"),value:d.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),S(n=>({...n,contrast:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.brightness"),value:d.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),S(n=>({...n,brightness:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.saturation"),value:d.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),S(n=>({...n,saturation:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.sharpness"),value:d.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),S(n=>({...n,sharpness:Number(t.toFixed(1))}))}})]}),aa!=null&&e.jsx("p",{className:"mt-2 text-[11px] text-tertiary",children:a("cam.controls.manualExposureFpsLimit",{exposure:d.exposureSeconds.toFixed(4),fps:aa.toFixed(2)})}),!ra&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ma=r==null?void 0:r.info)==null?void 0:Ma.actual_digital_gain)??d.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ee,onClick:()=>void nr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:d.autoExposure?"auto":"manual",onChange:t=>{S(n=>({...n,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:d.colorMode,onChange:t=>{S(n=>({...n,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:d.whiteBalanceMode,onChange:t=>{S(n=>({...n,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:yr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:d.aeFlickerMode,onChange:t=>{S(n=>({...n,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:1,step:.01,value:d.autoExposureMaxSeconds,onChange:t=>{S(n=>({...n,autoExposureMaxSeconds:N(Number(t.target.value)||1,.01,1)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(X,{label:"R Gain",value:d.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(n=>({...n,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(X,{label:"B Gain",value:d.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(n=>({...n,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!ft&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void sr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(ue||me)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[ue&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:ue}),me&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:me})]})]})}Fr.createRoot(document.getElementById("root")).render(e.jsx(Er.StrictMode,{children:e.jsx(Br,{children:e.jsx(Ar,{children:e.jsx(en,{})})})})); diff --git a/web/static/analysis-lab/assets/camera-q9IdHzgf.js b/web/static/analysis-lab/assets/camera-q9IdHzgf.js deleted file mode 100644 index 5f9546c..0000000 --- a/web/static/analysis-lab/assets/camera-q9IdHzgf.js +++ /dev/null @@ -1,61 +0,0 @@ -import{r as o,j as e,a as Sr,R as kr}from"./client-D1ZVDB-N.js";import{c as T,u as _r,T as Cr,I as Fr}from"./index-BoSUzpvv.js";import{u as Er,S as Rr,C as Ba,X as $r,r as Br,b as Pr}from"./http-CaEmWEuL.js";import{R as Mr}from"./rotate-ccw-D6okH6-S.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 Pa=T("Circle",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** - * @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 yt=T("Crosshair",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12",key:"l9bcsi"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12",key:"13hhkx"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2",key:"10w3f3"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18",key:"15g9kq"}]]);/** - * @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 Tr=T("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** - * @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 Ir=T("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);/** - * @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 Ar=T("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);/** - * @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 Or=T("Info",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 16v-4",key:"1dtifu"}],["path",{d:"M12 8h.01",key:"e9boi3"}]]);/** - * @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 Hr=T("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** - * @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 Ma=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);/** - * @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 Dr=T("Settings2",[["path",{d:"M20 7h-9",key:"3s1dr2"}],["path",{d:"M14 17H5",key:"gfn3mx"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]]);/** - * @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 Gr=T("SlidersHorizontal",[["line",{x1:"21",x2:"14",y1:"4",y2:"4",key:"obuewd"}],["line",{x1:"10",x2:"3",y1:"4",y2:"4",key:"1q6298"}],["line",{x1:"21",x2:"12",y1:"12",y2:"12",key:"1iu8h1"}],["line",{x1:"8",x2:"3",y1:"12",y2:"12",key:"ntss68"}],["line",{x1:"21",x2:"16",y1:"20",y2:"20",key:"14d8ph"}],["line",{x1:"12",x2:"3",y1:"20",y2:"20",key:"m0wm8r"}],["line",{x1:"14",x2:"14",y1:"2",y2:"6",key:"14e1ph"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14",key:"1i6ji0"}],["line",{x1:"16",x2:"16",y1:"18",y2:"22",key:"1lctlv"}]]);/** - * @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 zr=T("Square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);/** - * @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 Lr=T("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]),qr="/api/dev/debug";function H(a){return`${qr}${a}`}async function b(a,g={}){const f=a.startsWith("/api/debug/")?a.replace("/api/debug/","/api/dev/debug/"):a;return Br(f,g)}const Ur=["640x360","1280x720","1600x900","1920x1020"],Wr=[0,90,180,270],Je=12,ce=1e-4,le=.6,Vr=1e-4;function N(a,g,f){return Math.min(f,Math.max(g,a))}function _(a,g){return a==null||Number.isNaN(Number(a))?g:Number(a)}function de(a,g){return _(a,g)/1e6}function wt(a){return Math.round(a*1e6)}function jt(a){return`${de(a,0).toFixed(4)} s`}function Ta(a){const{exposureSeconds:g,autoExposureMaxSeconds:f,...p}=a;return{...p,exposure:wt(g),autoExposureMaxUs:wt(f)}}function Ia(a){if(!a)return"0 B";const g=["B","KB","MB","GB"];let f=0,p=a;for(;p>=1024&&fE.hfd),p=Math.min(...f),n=Math.max(...f),J=Math.max(.2,n-p),m=f.map((E,K)=>{const ue=K/Math.max(1,f.length-1)*320,l=76-(E-p)/J*60;return`${ue.toFixed(1)},${l.toFixed(1)}`}).join(" ");return e.jsxs("svg",{viewBox:"0 0 320 88",className:"h-24 w-full rounded-lg border border-outline-variant/20 bg-black/25",role:"img",children:[e.jsx("line",{x1:"0",y1:"76",x2:"320",y2:"76",stroke:"currentColor",opacity:"0.18"}),e.jsx("polyline",{points:m,fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinejoin:"round",className:"text-primary"}),e.jsx("circle",{cx:"320",cy:((B=m.split(" ").at(-1))==null?void 0:B.split(",")[1])??"44",r:"4",fill:"currentColor",className:"text-primary"})]})}async function Aa(){try{const a=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!a.ok)return"fail";const g=await a.json(),f=Number(g.max_clients??0),p=Number(g.active_clients??0);return f>0&&p>=f?"busy":"ok"}catch{return"fail"}}function X({label:a,value:g,min:f,max:p,step:n,onChange:J,disabled:m=!1,unit:B=""}){const E=n>=1?0:Math.min(6,Math.max(1,Math.ceil(-Math.log10(n))));return e.jsxs("label",{className:`block ${m?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"font-mono text-[11px]",children:[g.toFixed(E),B]})]}),e.jsx("input",{type:"range",min:f,max:p,step:n,value:g,disabled:m,onChange:K=>J(Number(K.target.value)),className:"w-full accent-primary"})]})}function Jr({label:a,value:g,disabled:f,onChange:p}){const n=N(g,ce,le),J=Math.log10(ce),m=Math.log10(le);return e.jsxs("div",{className:`block ${f?"opacity-50":""}`,children:[e.jsxs("div",{className:"mb-1 flex items-center justify-between gap-2",children:[e.jsx("span",{children:a}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("input",{type:"number",min:ce,max:le,step:Vr,value:Number(n.toFixed(6)),disabled:f,"aria-label":a,onChange:B=>{const E=Number(B.target.value);Number.isFinite(E)&&p(N(E,ce,le))},className:"w-24 rounded border border-outline-variant/30 bg-surface-container-low px-1.5 py-0.5 text-right font-mono text-[11px]"}),e.jsx("span",{className:"font-mono text-[11px]",children:"s"})]})]}),e.jsx("input",{type:"range",min:J,max:m,step:.001,value:Math.log10(n),disabled:f,"aria-label":a,onChange:B=>{const E=Number((10**Number(B.target.value)).toFixed(6));p(N(E,ce,le))},className:"w-full accent-primary"})]})}function Kr(){var ua,ma,xa,pa,ha,fa,ba,ga,va,ya,ja,wa,Na,Sa,ka,_a,Ca,Fa,Ea,Ra;const{t:a,locale:g,setLocale:f}=_r(),{info:p}=Er(),[n,J]=o.useState(null),[m,B]=o.useState(!1),[E,K]=o.useState(()=>Date.now()),[ue,l]=o.useState(null),[we,Q]=o.useState(null),[Nt,ee]=o.useState(!1),[me,v]=o.useState(null),[I,Ne]=o.useState(!1),[Ke,St]=o.useState(!1),[kt,_t]=o.useState(!1),[Ye,Ct]=o.useState("8"),[Ft,Et]=o.useState("8"),[Rt,$t]=o.useState("1280x720"),[Bt,Pt]=o.useState("supersample"),[xe,te]=o.useState(!1),[Mt,Oa]=o.useState(!0),[Ze,Ha]=o.useState(!0),[Qe,Da]=o.useState(!1),[Tt,Ga]=o.useState(!1),[It,At]=o.useState(!1),[et,za]=o.useState({mean:0,std:0,over:0}),[La,Se]=o.useState(0),[i,tt]=o.useState(null),[k,at]=o.useState(!1),[ae,ke]=o.useState(!1),[s,Ot]=o.useState(null),[_e,pe]=o.useState([]),[re,he]=o.useState(null),[j,rt]=o.useState(null),[Ht,D]=o.useState(null),[Dt,Gt]=o.useState(0),[zt,Lt]=o.useState(180),[Ce,qt]=o.useState(!1),[Fe,Ut]=o.useState(!1),[d,S]=o.useState({exposureSeconds:.005,gain:1,digitalGain:1,autoExposure:!0,contrast:1,brightness:0,saturation:1,sharpness:1,noiseReduction:0,noiseReductionMode:"fast",aeFlickerMode:"off",autoExposureMaxSeconds:.6,whiteBalanceMode:"auto",whiteBalanceGainR:1,whiteBalanceGainB:1,colorMode:"color"}),[Ee,w]=o.useState(!1),[nt,Wt]=o.useState(""),[Vt,Xt]=o.useState(""),[Jt,qa]=o.useState([]),[st,L]=o.useState(!1),[fe,Ua]=o.useState([]),[Kt,Yt]=o.useState(!1),[C,ot]=o.useState(null),[Wa,it]=o.useState(!1),[Re,ct]=o.useState(null),[lt,$e]=o.useState(1),P=o.useRef(null),Be=o.useRef(null),be=o.useRef(null),Pe=o.useRef(null),Me=o.useRef(null),q=o.useRef(null),Te=o.useRef(null),dt=o.useRef(!1),ne=o.useRef(!1),Y=o.useRef(!1),Ie=o.useRef(null),U=o.useRef([]),se=o.useRef(0),ut=o.useRef(null),Zt=()=>{U.current=[],se.current=0,Ie.current=null,Se(0)},R=async()=>{try{const t=await b("/api/debug/camera/status",{cache:"no-store"});J(t),t.streaming||(B(!1),Y.current=!1)}catch(t){l(t instanceof Error?t.message:String(t))}},ge=()=>{Te.current!=null&&(window.clearTimeout(Te.current),Te.current=null)},mt=async()=>{if(I)return!1;Ne(!0),l(null),Q(null),ee(!1);try{ge(),n!=null&&n.streaming||await b("/api/debug/camera/start",{method:"POST"});const t=Date.now(),r=await Aa();return r==="busy"?(Q(a("cam.err.streamBusy")),ee(!0),!1):r==="fail"?(Q(a("cam.err.streamProbeFailed")),ee(!1),!1):(B(!0),Y.current=!0,v(a("cam.notice.previewStart")),Zt(),Ie.current=performance.now(),K(t),await R(),!0)}catch(t){return l(t instanceof Error?t.message:String(t)),!1}finally{Ne(!1)}},Qt=async()=>{if(!ae){ke(!0),D(null);try{if(!(m||await mt()))return;const r=await b(H("/camera/focus/session/start"),{method:"POST"});ne.current=r.active,S(c=>({...c,autoExposure:!1,exposureSeconds:de(r.locked.exposure,5e3),gain:r.locked.gain,digitalGain:r.locked.digitalGain,noiseReduction:0,noiseReductionMode:r.locked.noiseReductionMode})),w(!1),Ot(null),pe([]),he(null),rt(null),At(!0),at(!0),window.setTimeout(()=>{var c;(c=document.getElementById("focus-calibration-panel"))==null||c.scrollIntoView({behavior:"smooth",block:"start"})},0)}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},xt=async()=>{if(!ae){ke(!0),at(!1),D(null);try{if(ne.current){const t=await b(H("/camera/focus/session/stop"),{method:"POST"});ne.current=!1,t.restored_settings&&(S(r=>({...r,autoExposure:t.restored_settings.autoExposure,exposureSeconds:de(t.restored_settings.exposure,5e3),gain:t.restored_settings.gain,digitalGain:t.restored_settings.digitalGain,noiseReductionMode:t.restored_settings.noiseReductionMode,noiseReduction:t.restored_settings.noiseReductionMode==="off"?0:1})),w(!1))}}catch(t){D(t instanceof Error?t.message:String(t))}finally{ke(!1)}}},Va=()=>{pe([]),he(null),D(null)},Xa=t=>{if(!k||!P.current||t.target.closest("button, label, input"))return;const r=P.current,c=t.currentTarget.getBoundingClientRect(),h=r.naturalWidth||(s==null?void 0:s.frame.width)||0,u=r.naturalHeight||(s==null?void 0:s.frame.height)||0;if(h<=0||u<=0)return;const x=Math.min(c.width/h,c.height/u),F=h*x,y=u*x,$=(c.width-F)/2,M=(c.height-y)/2,z=(t.clientX-c.left-$)/F,oe=(t.clientY-c.top-M)/y;z<0||z>1||oe<0||oe>1||(rt({x:z,y:oe}),pe([]),he(null),D(null))},Ja=()=>{rt(null),pe([]),he(null),D(null)},Ka=async()=>{if(!I){Ne(!0),l(null);try{ge(),B(!1),Y.current=!1,Q(null),ee(!1),Zt(),P.current&&(P.current.onload=null,P.current.onerror=null,P.current.src="",P.current.removeAttribute("src")),K(Date.now()),v(a("cam.notice.previewStop"))}catch(t){l(t instanceof Error?t.message:String(t))}finally{Ne(!1)}}},Ya=async()=>{if(!(!m&&!(n!=null&&n.streaming))){_t(!0),l(null);try{const t=await b("/api/debug/camera/capture",{method:"POST"});v(a("cam.notice.captureSaved",{name:t.filename||"capture"}))}catch(t){l(t instanceof Error?t.message:String(t))}finally{_t(!1)}}},Za=async()=>{if(!Ke){St(!0),l(null);try{if(n!=null&&n.recording)await b("/api/debug/camera/record/stop",{method:"POST"}),v(a("cam.notice.recordStop"));else{const t=await b("/api/debug/camera/record/start",{method:"POST"});v(a("cam.notice.recordStart",{name:t.filename||"video.avi"}))}await R()}catch(t){l(t instanceof Error?t.message:String(t))}finally{St(!1)}}},Qa=async()=>{l(null);try{const t=N(parseInt(Ye,10)||5,1,60);await b(`/api/debug/camera/fps?fps=${t}`,{method:"POST"});const r=N(parseInt(Ft,10)||8,1,30);await b(`/api/debug/camera/preview-fps?fps=${r}`,{method:"POST"});const[c,h]=Rt.split("x").map(u=>parseInt(u,10));c&&h&&await b(`/api/debug/camera/size?width=${c}&height=${h}`,{method:"POST"}),await b(`/api/debug/camera/sampling?mode=${encodeURIComponent(Bt)}`,{method:"POST"}),v(a("cam.notice.runtimeApplied")),te(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},er=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.settingsApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},tr=async()=>{l(null);try{await b("/api/debug/camera/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ta(d))}),v(a("cam.notice.modeApplied")),w(!1),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},ar=(t,r)=>{if(!t)return;const c=(r==null?void 0:r.syncRuntime)??!0;S({exposureSeconds:N(de(t.exposure_us,5e3),ce,le),gain:N(_(t.analogue_gain,1),1,24),digitalGain:N(_(t.digital_gain,1),1,8),autoExposure:!!(t.auto_exposure??!0),contrast:N(_(t.contrast,1),0,2),brightness:N(_(t.brightness,0),-1,1),saturation:N(_(t.saturation,1),0,2),sharpness:N(_(t.sharpness,1),0,2),noiseReduction:N(Math.round(_(t.noise_reduction,0)),0,4),noiseReductionMode:String(t.noise_reduction_mode??"fast"),aeFlickerMode:String(t.ae_flicker_mode??"off"),autoExposureMaxSeconds:N(de(t.auto_exposure_max_us,6e5),.01,.6),whiteBalanceMode:String(t.white_balance_mode??"auto"),whiteBalanceGainR:N(_(t.white_balance_gain_r,1),.1,3),whiteBalanceGainB:N(_(t.white_balance_gain_b,1),.1,3),colorMode:String(t.color_mode??"color")}),c&&(Ct(String(Math.round(_(t.fps,8)))),$t(`${Math.round(_(t.width,1280))}x${Math.round(_(t.height,720))}`),Pt(String(t.sampling_mode??"supersample")),te(!1)),Lt(N(Math.round(_(t.rotation,180)),0,270)),qt(!!t.flip_horizontal),Ut(!!t.flip_vertical),w(!1)},ea=async(t,r)=>{l(null);try{await b("/api/debug/camera/mirror",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({flip_horizontal:t,flip_vertical:r})}),qt(t),Ut(r),v(a("cam.notice.mirrorApplied")),await R()}catch(c){l(c instanceof Error?c.message:String(c))}},rr=async t=>{l(null);try{await b(`/api/debug/camera/rotation/${t}`,{method:"POST"}),Lt(t),v(a("cam.notice.rotationApplied",{value:t})),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},ta=async t=>{l(null);try{await b(`/api/debug/camera/night-mode?enabled=${t?"true":"false"}`,{method:"POST"}),v(a(t?"cam.notice.nightOn":"cam.notice.nightOff")),await R()}catch(r){l(r instanceof Error?r.message:String(r))}},nr=async()=>{if(window.confirm(a("cam.confirm.reset"))){l(null);try{await b("/api/debug/camera/reset",{method:"POST"}),v(a("cam.notice.reset")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}}},sr=async()=>{l(null);try{await b("/api/debug/camera/backup-settings",{method:"POST"}),v(a("cam.notice.backup"))}catch(t){l(t instanceof Error?t.message:String(t))}},or=async()=>{l(null);try{await b("/api/debug/camera/restore-settings",{method:"POST"}),v(a("cam.notice.restore")),await R()}catch(t){l(t instanceof Error?t.message:String(t))}},Ae=async()=>{L(!0);try{const t=await b("/api/debug/camera/presets",{cache:"no-store"});qa(t.presets??[])}catch(t){l(t instanceof Error?t.message:String(t))}finally{L(!1)}},ir=async()=>{const t=nt.trim();if(t){L(!0),l(null);try{await b("/api/debug/camera/presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,description:Vt.trim(),exposure_us:wt(d.exposureSeconds),analogue_gain:d.gain,digital_gain:d.digitalGain,auto_exposure:d.autoExposure,contrast:d.contrast,brightness:d.brightness,saturation:d.saturation,sharpness:d.sharpness,noise_reduction:d.noiseReduction,white_balance_mode:d.whiteBalanceMode,white_balance_gain_r:d.whiteBalanceGainR,white_balance_gain_b:d.whiteBalanceGainB,rotation:zt,flip_horizontal:Ce,flip_vertical:Fe,color_mode:d.colorMode})}),v(a("cam.notice.presetSaved",{name:t})),Wt(""),Xt(""),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},cr=async t=>{L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}/apply`,{method:"POST"}),v(a("cam.notice.presetApplied",{name:t})),await R(),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}},lr=async t=>{if(window.confirm(a("cam.confirm.deletePreset",{name:t}))){L(!0),l(null);try{await b(`/api/debug/camera/presets/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.presetDeleted",{name:t})),await Ae()}catch(r){l(r instanceof Error?r.message:String(r))}finally{L(!1)}}},pt=async()=>{Yt(!0);try{const t=await b("/api/debug/files",{cache:"no-store"});Ua(t.files??[]),$e(1)}catch(t){l(t instanceof Error?t.message:String(t))}finally{Yt(!1)}},ht=()=>{ct(null),ot(null),it(!1)},dr=async t=>{if(Re===t){ht();return}ct(t),ot(null),it(!0),l(null);try{const r=await b(`/api/debug/files/${encodeURIComponent(t)}/info`,{cache:"no-store"});ot(r)}catch(r){l(r instanceof Error?r.message:String(r)),ct(null)}finally{it(!1)}},ur=t=>{const r=(h,u)=>{const x=document.createElement("a");x.href=u,x.download=h,document.body.appendChild(x),x.click(),document.body.removeChild(x)};r(t,`${H("/files")}/${encodeURIComponent(t)}`);const c=t.match(/\.(jpe?g|png|bmp|tiff?|webp|mp4|avi|mov|mkv|wmv|flv|webm|m4v)$/i);if(c){const u=`${t.slice(0,-c[0].length)}.txt`;(async()=>{try{if(!(await fetch(`${H("/files")}/${encodeURIComponent(u)}`)).ok)return;r(u,`${H("/files")}/${encodeURIComponent(u)}`),v(a("cam.notice.downloadWithSidecar",{name:t,sidecar:u}))}catch{v(a("cam.notice.download",{name:t}))}})();return}v(a("cam.notice.download",{name:t}))},mr=async t=>{if(window.confirm(a("cam.confirm.deleteFile",{name:t}))){l(null);try{await b(`/api/debug/files/${encodeURIComponent(t)}`,{method:"DELETE"}),v(a("cam.notice.fileDeleted",{name:t})),(Re===t||(C==null?void 0:C.filename)===t)&&ht(),await pt()}catch(r){l(r instanceof Error?r.message:String(r))}}},xr=()=>{if(!Mt||!P.current||!Be.current)return;const t=P.current;if(!t.naturalWidth||!t.naturalHeight||(be.current||(be.current=document.createElement("canvas"),Pe.current=be.current.getContext("2d",{willReadFrequently:!0})),Me.current||(Me.current=Be.current.getContext("2d")),!Pe.current||!Me.current))return;const c=Math.min(1,320/t.naturalWidth),h=Math.max(1,Math.round(t.naturalWidth*c)),u=Math.max(1,Math.round(t.naturalHeight*c));be.current.width=h,be.current.height=u,Pe.current.drawImage(t,0,0,h,u);const x=Pe.current.getImageData(0,0,h,u).data,F=new Array(256).fill(0),y=new Array(256).fill(0),$=new Array(256).fill(0),M=new Array(256).fill(0);let z=0,oe=0,$a=0;const ie=h*u;for(let O=0;O=250&&($a+=1)}const vt=ie?z/ie:0,wr=ie?oe/ie-vt*vt:0;za({mean:vt,std:Math.sqrt(Math.max(0,wr)),over:ie?$a/ie*100:0});const Ue=Be.current,A=Me.current,We=window.devicePixelRatio||1,ve=Math.max(1,Ue.clientWidth||240),ye=Math.max(1,Ue.clientHeight||120);Ue.width=Math.floor(ve*We),Ue.height=Math.floor(ye*We),A.setTransform(We,0,0,We,0,0),A.clearRect(0,0,ve,ye);const Nr=Math.max(1,...Ze?[Math.max(...F),Math.max(...y),Math.max(...$)]:[0],...Qe?[Math.max(...M)]:[0]),Ve=(O,Xe)=>{A.beginPath(),A.strokeStyle=Xe,A.lineWidth=1.2;for(let W=0;W<256;W+=1){const je=W/255*ve,V=ye-O[W]/Nr*ye;W===0?A.moveTo(je,V):A.lineTo(je,V)}A.stroke()};if(Ze&&(Ve(F,"rgba(255,80,80,0.85)"),Ve(y,"rgba(80,255,80,0.85)"),Ve($,"rgba(80,160,255,0.85)")),Qe&&Ve(M,"rgba(255,255,255,0.95)"),Tt){const O=.9803921568627451*ve;A.fillStyle="rgba(255,100,100,0.12)",A.fillRect(O,0,ve-O,ye)}};o.useEffect(()=>{R(),Ae(),pt();const t=window.setInterval(()=>{document.hidden||R()},2e3);return()=>window.clearInterval(t)},[n==null?void 0:n.streaming]),o.useEffect(()=>{!(n!=null&&n.info)||Ee||ar(n.info,{syncRuntime:!xe})},[n==null?void 0:n.info,Ee,xe]),o.useEffect(()=>{if(!(n!=null&&n.recording)){q.current&&(window.clearInterval(q.current),q.current=null),Gt(0);return}if(q.current)return;const t=Date.now();return q.current=window.setInterval(()=>{Gt(Math.max(0,Math.floor((Date.now()-t)/1e3)))},1e3),()=>{q.current&&(window.clearInterval(q.current),q.current=null)}},[n==null?void 0:n.recording]),o.useEffect(()=>{Y.current=m,m||(ge(),at(!1),ne.current&&xt())},[m]),o.useEffect(()=>()=>{ne.current&&(navigator.sendBeacon(H("/camera/focus/session/stop")),ne.current=!1)},[]),o.useEffect(()=>{if(!k||!m)return;let t=!1;const r=async()=>{var h,u;if(!(dt.current||document.hidden)){dt.current=!0;try{const x=new URLSearchParams;j&&(x.set("target_x",j.x.toFixed(6)),x.set("target_y",j.y.toFixed(6)));const F=x.size>0?`?${x.toString()}`:"",y=await b(`${H("/camera/focus/metrics")}${F}`,{cache:"no-store"});if(t)return;Ot(y),D(null);const $=j?(h=y.selected_star)==null?void 0:h.hfd_px:(u=y.aggregate)==null?void 0:u.median_hfd_px;typeof $=="number"&&Number.isFinite($)&&(pe(M=>{var z;return((z=M.at(-1))==null?void 0:z.frameId)===y.frame_id?M:[...M,{frameId:y.frame_id,hfd:$}].slice(-60)}),he(M=>M==null?$:Math.min(M,$)))}catch(x){t||D(x instanceof Error?x.message:String(x))}finally{dt.current=!1}}};r();const c=window.setInterval(r,850);return()=>{t=!0,window.clearInterval(c)}},[k,m,j==null?void 0:j.x,j==null?void 0:j.y]),o.useEffect(()=>{m&&(U.current=[],se.current=0)},[E,m]),o.useEffect(()=>{if(!m){U.current=[],se.current=0,Se(0);return}if(!ut.current){const u=document.createElement("canvas");u.width=32,u.height=32,ut.current=u}const r=ut.current.getContext("2d",{willReadFrequently:!0});if(!r)return;let c=0;const h=()=>{const u=P.current;if(u!=null&&u.complete&&u.naturalWidth>0)try{r.drawImage(u,0,0,32,32);const x=r.getImageData(0,0,32,32).data;let F=2166136261;for(let y=0;ywindow.cancelAnimationFrame(c)},[m]),o.useEffect(()=>{if(!m){Se(0);return}const t=window.setInterval(()=>{const c=performance.now()-1e3,h=U.current;for(;h.length&&h[0]window.clearInterval(t)},[m]),o.useEffect(()=>{if(!m){tt(null);return}let t=!1;const r=async()=>{try{const h=await fetch(`${H("/camera/stream/status")}`,{cache:"no-store",credentials:"same-origin"});if(!h.ok||t)return;const u=await h.json();if(!t){tt(u);const x=Number(u.preview_target_fps??u.target_preview_fps??0);Number.isFinite(x)&&x>0&&!xe&&Et(String(Math.round(x)))}}catch{t||tt(null)}};r();const c=window.setInterval(r,2e3);return()=>{t=!0,window.clearInterval(c)}},[m,xe]),o.useEffect(()=>{if(!me)return;const t=window.setTimeout(()=>v(null),3200);return()=>window.clearTimeout(t)},[me]),o.useEffect(()=>()=>ge(),[]),o.useEffect(()=>{const t=Math.max(1,Math.ceil(fe.length/Je));lt>t&&$e(t)},[fe.length,lt]);const pr=m?`${H("/camera/stream")}?t=${E}`:"",Oe=d.autoExposure,ft=d.whiteBalanceMode==="manual",hr=N(parseInt(Ye,10)||Math.round(_((ua=n==null?void 0:n.info)==null?void 0:ua.fps,8)),1,60),aa=!d.autoExposure&&1/d.exposureSeconds0?Z.awb_modes:["auto","daylight","cloudy","tungsten","fluorescent","indoor","manual","night"],br=Array.isArray(Z.noise_reduction_modes)&&Z.noise_reduction_modes.length>0?Z.noise_reduction_modes:["off","fast","high_quality"],ra=Z.manual_digital_gain!==!1,na=!!((xa=n==null?void 0:n.info)!=null&&xa.night_mode),sa=m,He=!I&&!m&&!(n!=null&&n.recording),oa=!I&&m,gr=!I&&!kt&&sa,vr=!I&&!Ke&&sa,G=j?s==null?void 0:s.selected_star:null,De=j?(G==null?void 0:G.hfd_px)??null:((pa=s==null?void 0:s.aggregate)==null?void 0:pa.median_hfd_px)??null,ia=j?(G==null?void 0:G.fwhm_px)??null:((ha=s==null?void 0:s.aggregate)==null?void 0:ha.median_fwhm_px)??null,ca=j?(G==null?void 0:G.concentration)??null:((fa=s==null?void 0:s.aggregate)==null?void 0:fa.median_concentration)??null,Ge=De!=null&&re!=null&&re>0?(De-re)/re*100:null,la=_e.slice(-3).map(t=>t.hfd),bt=_e.slice(-6,-3).map(t=>t.hfd),gt=t=>t.reduce((r,c)=>r+c,0)/Math.max(1,t.length);let ze="cam.focus.guidance.collecting";if((s==null?void 0:s.state)==="no_stars")ze="cam.focus.guidance.noStars";else if(_e.length>=5&&Ge!=null&&Ge<=3)ze="cam.focus.guidance.best";else if(la.length===3&&bt.length===3){const t=gt(la)-gt(bt),r=Math.max(.04,gt(bt)*.02);ze=t<-r?"cam.focus.guidance.improving":t>r?"cam.focus.guidance.worsening":"cam.focus.guidance.steady"}const yr=((s==null?void 0:s.stars_used)??0)>=5?"cam.focus.confidence.high":((s==null?void 0:s.stars_used)??0)>=3?"cam.focus.confidence.medium":"cam.focus.confidence.low",Le=Math.max(1,Math.ceil(fe.length/Je)),qe=Math.min(lt,Le),da=(qe-1)*Je,jr=fe.slice(da,da+Je);return e.jsxs("div",{className:"min-h-screen bg-background text-on-surface",children:[e.jsx("header",{className:"sticky top-0 z-30 border-b border-outline-variant/20 bg-surface-container-low/90 px-4 py-3 backdrop-blur",children:e.jsxs("div",{className:"flex w-full flex-wrap items-center justify-between gap-3",children:[e.jsx("div",{className:"min-w-0",children:e.jsx("h1",{className:"break-words font-headline text-lg font-bold text-primary sm:text-xl",children:`OGScope ${a("cam.title")}`})}),e.jsxs("div",{className:"flex items-center gap-2 text-xs",children:[e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="zh"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("zh"),children:a("lang.zh")}),e.jsx("button",{type:"button",className:`inline-flex h-7 items-center rounded px-2 py-1 ${g==="en"?"bg-primary-container text-on-primary-container":"text-on-surface-variant"}`,onClick:()=>f("en"),children:a("lang.en")}),e.jsxs("a",{href:"/debug",className:"inline-flex h-7 items-center gap-1 rounded border border-outline-variant/30 px-2 py-1 hover:bg-surface-container",children:[e.jsx(Dr,{className:"h-3.5 w-3.5"})," ",a("cam.btn.system")]})]})]})}),e.jsxs("main",{className:"mx-auto grid max-w-[1880px] grid-cols-12 gap-4 p-4",children:[e.jsxs("aside",{className:"order-3 col-span-12 space-y-4 2xl:order-1 2xl:col-span-2",children:[e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.tools")}),e.jsx("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:Wr.map(t=>e.jsxs("button",{type:"button",onClick:()=>void rr(t),className:`rounded border px-2 py-1 ${zt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:[t,"°"]},t))}),e.jsx("div",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.mirror.hint")}),e.jsxs("div",{className:"mb-2 flex flex-wrap gap-1 text-xs",children:[e.jsx("button",{type:"button",onClick:()=>void ea(!Ce,Fe),className:`rounded border px-2 py-1 ${Ce?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.horizontal")}),e.jsx("button",{type:"button",onClick:()=>void ea(Ce,!Fe),className:`rounded border px-2 py-1 ${Fe?"border-primary text-primary":"border-outline-variant/30"}`,children:a("cam.mirror.vertical")})]}),e.jsxs("div",{className:"flex flex-wrap gap-2 text-xs",children:[e.jsxs("button",{type:"button",onClick:()=>void sr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Rr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.backup")]}),e.jsxs("button",{type:"button",onClick:()=>void or(),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Ar,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.restore")]}),e.jsx("button",{type:"button",onClick:()=>void nr(),className:"rounded border border-outline-variant/40 px-2 py-1",children:a("cam.controls.reset")})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.quick.title")}),e.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e.jsx("span",{className:"text-[11px] text-on-surface-variant",children:a("cam.quick.nightHint")}),e.jsxs("button",{type:"button",onClick:()=>void ta(!0),className:`rounded border px-2 py-1 ${na?"border-primary/70 text-primary":"border-outline-variant/40"}`,children:[e.jsx(Hr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOn")]}),e.jsxs("button",{type:"button",onClick:()=>void ta(!1),className:`rounded border px-2 py-1 ${na?"border-error/60 text-error":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Lr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.controls.nightOff")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.presets.title")}),e.jsxs("div",{className:"grid grid-cols-1 gap-2",children:[e.jsx("input",{value:nt,onChange:t=>Wt(t.target.value),placeholder:a("cam.presets.name"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"}),e.jsx("input",{value:Vt,onChange:t=>Xt(t.target.value),placeholder:a("cam.presets.desc"),className:"rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:st||!nt.trim(),onClick:()=>void ir(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.save")})}),e.jsxs("div",{className:"mt-3 max-h-48 space-y-2 overflow-auto",children:[Jt.length===0&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.presets.empty")}),Jt.map(t=>e.jsxs("div",{className:"rounded border border-outline-variant/20 p-2",children:[e.jsx("div",{className:"font-semibold",children:t.name}),e.jsx("div",{className:"text-on-surface-variant",children:t.description||a("cam.presets.noDesc")}),e.jsxs("div",{className:"mt-1 text-on-surface-variant",children:[a("cam.controls.exposure"),": ",jt(t.exposure_us)," | ",a("cam.controls.gain"),": ",t.analogue_gain]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsx("button",{type:"button",disabled:st,onClick:()=>void cr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.apply")}),e.jsx("button",{type:"button",disabled:st,onClick:()=>void lr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.presets.delete")})]})]},t.name))]})]})]}),e.jsxs("section",{className:"order-1 col-span-12 grid grid-cols-12 items-start gap-4 2xl:order-2 2xl:col-span-10",children:[e.jsxs("div",{className:"col-span-12 space-y-4 2xl:col-span-9",children:[e.jsxs("section",{className:"self-start rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 grid grid-cols-1 gap-2 text-xs sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["CPU: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.cpu_usage)==null?"—":`${Number(p.cpu_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["MEM: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.memory_usage)==null?"—":`${Number(p.memory_usage).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1",children:["TEMP: ",e.jsx("span",{className:"font-mono",children:(p==null?void 0:p.temperature)==null?"—":`${Number(p.temperature).toFixed(1)}°C`})]})]}),e.jsxs("div",{className:"mb-2 flex flex-wrap items-start justify-between gap-2",children:[e.jsxs("div",{children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wider",children:a("cam.preview.title")}),e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.hint.mjpegSingleStream")})]}),e.jsxs("div",{className:"flex shrink-0 flex-wrap items-center justify-end gap-2",children:[e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>k?void xt():void Qt(),className:`rounded-lg px-2.5 py-1.5 text-xs font-medium disabled:opacity-50 ${k?"border border-primary/50 text-primary":"bg-primary-container text-on-primary-container"}`,children:[e.jsx(yt,{className:"mr-1 inline h-3.5 w-3.5"}),a(ae?"cam.focus.starting":k?"cam.focus.finish":"cam.focus.start")]}),e.jsxs("span",{className:"font-mono text-xs text-on-surface-variant",children:[a("cam.preview.state"),": ",a(m?"cam.state.streaming":"cam.state.idle")]})]})]}),we&&!m&&e.jsxs("div",{className:"mb-2 rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-left",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"mt-1 max-w-2xl text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),e.jsxs("div",{className:`relative aspect-video overflow-hidden rounded border border-outline-variant/20 bg-black ${k?"cursor-crosshair":""}`,onClick:Xa,children:[m&&we&&e.jsxs("div",{className:"pointer-events-none absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 bg-black/80 px-4 text-center",role:"alert",children:[e.jsx("p",{className:"text-sm font-medium text-error",children:we}),Nt?e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamBusyHint")}):e.jsx("p",{className:"max-w-md text-[11px] leading-snug text-on-surface-variant",children:a("cam.err.streamProbeDetail")})]}),m?e.jsx("img",{ref:P,alt:"camera-preview",className:"h-full w-full object-contain",src:pr,onLoad:()=>{Q(null),ee(!1),xr()},onError:()=>{var r;if(ge(),!Y.current)return;const t=(r=P.current)==null?void 0:r.src;(async()=>{if(!Y.current||!t)return;if(await Aa()==="busy"){Q(a("cam.err.streamBusy")),ee(!0);return}Te.current=window.setTimeout(()=>{Y.current&&K(Date.now())},400)})()}}):e.jsxs("div",{className:"flex h-full w-full flex-col items-center justify-center gap-3 text-center text-on-surface-variant",children:[e.jsx(Ba,{className:"h-10 w-10 text-primary/80"}),e.jsx("div",{className:"text-sm",children:a("cam.preview.emptyTitle")}),e.jsx("div",{className:"text-xs",children:a("cam.preview.emptyDesc")}),e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-1.5 text-xs disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"mr-1 inline h-3.5 w-3.5"}),a(I?"cam.btn.starting":"cam.btn.start")]})]}),m&&k&&(s!=null&&s.frame)?e.jsxs("svg",{className:"pointer-events-none absolute inset-0 z-10 h-full w-full",viewBox:`0 0 ${s.frame.width} ${s.frame.height}`,preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:[s.stars.map((t,r)=>{const c=s.selected_star&&Math.abs(s.selected_star.x-t.x)<1&&Math.abs(s.selected_star.y-t.y)<1;return e.jsx("circle",{cx:t.x,cy:t.y,r:c?16:10,fill:"none",stroke:c?"#facc15":"#38bdf8",strokeWidth:c?3:2,vectorEffect:"non-scaling-stroke"},`${s.frame_id}-${r}`)}),j?e.jsxs("g",{transform:`translate(${j.x*s.frame.width} ${j.y*s.frame.height})`,stroke:"#facc15",strokeWidth:"2",vectorEffect:"non-scaling-stroke",children:[e.jsx("line",{x1:"-12",y1:"0",x2:"12",y2:"0"}),e.jsx("line",{x1:"0",y1:"-12",x2:"0",y2:"12"})]}):null]}):null,(n==null?void 0:n.recording)&&e.jsxs("div",{className:"absolute right-3 top-3 flex items-center gap-2 rounded bg-black/60 px-2 py-1 text-xs text-error",children:[e.jsx(Pa,{className:"h-3.5 w-3.5 fill-current"})," ",a("cam.state.rec"),e.jsx("span",{className:"font-mono",children:`${Math.floor(Dt/60).toString().padStart(2,"0")}:${(Dt%60).toString().padStart(2,"0")}`})]}),e.jsx("div",{className:"absolute left-3 top-3",children:e.jsx("button",{type:"button",onClick:()=>At(t=>!t),className:"rounded border border-outline-variant/40 bg-black/70 px-2 py-1 text-xs text-white",children:a(It?"cam.hist.expand":"cam.hist.collapse")})}),!It&&e.jsxs("div",{className:"absolute left-3 top-12 w-[360px] max-w-[calc(100%-1.5rem)] rounded border border-outline-variant/30 bg-black/70 p-2 text-white",children:[e.jsxs("div",{className:"mb-2 flex flex-wrap items-center gap-3 text-[11px]",children:[e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Mt,onChange:t=>Oa(t.target.checked)})," ",a("cam.hist.enabled")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Ze,onChange:t=>Ha(t.target.checked)})," RGB"]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Qe,onChange:t=>Da(t.target.checked)})," ",a("cam.hist.luminance")]}),e.jsxs("label",{className:"inline-flex items-center gap-1",children:[e.jsx("input",{type:"checkbox",checked:Tt,onChange:t=>Ga(t.target.checked)})," ",a("cam.hist.over")]})]}),e.jsx("canvas",{ref:Be,className:"h-24 w-full rounded border border-white/20 bg-black/60"}),e.jsxs("div",{className:"mt-2 grid grid-cols-3 gap-2 text-[11px]",children:[e.jsxs("div",{children:["mean: ",e.jsx("span",{className:"font-mono",children:et.mean.toFixed(1)})]}),e.jsxs("div",{children:["std: ",e.jsx("span",{className:"font-mono",children:et.std.toFixed(1)})]}),e.jsxs("div",{children:["over: ",e.jsxs("span",{className:"font-mono",children:[et.over.toFixed(2),"%"]})]})]})]})]}),e.jsxs("section",{id:"focus-calibration-panel",className:`mt-3 scroll-mt-20 rounded-xl border p-3 ${k?"border-primary/45 bg-primary-container/10":"border-outline-variant/20 bg-surface-container-low"}`,children:[e.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx(yt,{className:"h-5 w-5 text-primary"}),e.jsx("h3",{className:"text-sm font-semibold",children:a("cam.focus.title")}),e.jsx("span",{className:`rounded-full px-2 py-0.5 text-[10px] font-medium ${k?"bg-primary-container text-on-primary-container":"bg-surface-container-high text-on-surface-variant"}`,children:a(k?"cam.focus.active":"cam.focus.idle")})]}),e.jsx("p",{className:"mt-1 max-w-3xl text-[11px] leading-relaxed text-on-surface-variant",children:a("cam.focus.intro")}),k?e.jsx("p",{className:"mt-1 text-[10px] text-primary",children:a("cam.focus.sessionLocked")}):null]}),e.jsxs("div",{className:"flex flex-wrap gap-2",children:[k?e.jsx("button",{type:"button",onClick:()=>void xt(),className:"rounded-lg border border-error/50 px-3 py-2 text-xs text-error",children:a("cam.focus.finish")}):e.jsxs("button",{type:"button",disabled:ae||I||!!(n!=null&&n.recording),onClick:()=>void Qt(),className:"rounded-lg bg-primary-container px-3 py-2 text-xs font-medium text-on-primary-container disabled:opacity-50",children:[e.jsx(yt,{className:"mr-1 inline h-4 w-4"}),a(ae?"cam.focus.starting":"cam.focus.start")]}),e.jsxs("button",{type:"button",disabled:!k,onClick:Va,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:[e.jsx(Mr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.focus.resetBest")]}),e.jsx("button",{type:"button",disabled:!k||j==null,onClick:Ja,className:"rounded-lg border border-outline-variant/40 px-3 py-2 text-xs disabled:opacity-40",children:a("cam.focus.autoSelect")})]})]}),k?e.jsxs("div",{className:"mt-3 grid gap-3 xl:grid-cols-[minmax(0,1.25fr)_minmax(280px,0.75fr)]",children:[e.jsxs("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-4",children:[e.jsxs("div",{className:"rounded-lg border border-primary/30 bg-black/20 p-3 sm:col-span-2",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a(j?"cam.focus.currentHfd.single":"cam.focus.currentHfd.multi")}),e.jsxs("div",{className:"mt-1 flex items-end gap-2",children:[e.jsx("span",{className:"font-mono text-3xl font-semibold text-primary",children:De==null?"—":De.toFixed(2)}),e.jsxs("span",{className:"pb-1 text-xs text-on-surface-variant",children:["px · ",a("cam.focus.lowerBetter")]})]}),e.jsx("p",{className:"mt-2 rounded-md border border-outline-variant/20 bg-surface-container-low/70 px-2 py-1.5 text-xs font-medium",children:a(ze)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.bestHfd")}),e.jsx("div",{className:"mt-1 font-mono text-xl",children:re==null?"—":`${re.toFixed(2)} px`}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:Ge==null?"—":`+${Math.max(0,Ge).toFixed(1)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stars")}),e.jsxs("div",{className:"mt-1 font-mono text-xl",children:[(s==null?void 0:s.stars_used)??0," / ",(s==null?void 0:s.stars_measured)??0," / ",(s==null?void 0:s.stars_detected)??0]}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(yr)})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.fwhm")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ia==null?"—":`${ia.toFixed(2)} px`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.concentration")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:ca==null?"—":`${(ca*100).toFixed(0)}%`})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.selectedStar")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.selected_star?`HFD ${s.selected_star.hfd_px.toFixed(2)}`:"—"}),e.jsx("div",{className:"mt-1 text-[11px] text-on-surface-variant",children:a(j?"cam.focus.manualTarget":"cam.focus.autoTarget")})]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 p-3",children:[e.jsx("div",{className:"text-[10px] uppercase tracking-wider text-on-surface-variant",children:a("cam.focus.stability")}),e.jsx("div",{className:"mt-1 font-mono text-lg",children:s!=null&&s.aggregate?`±${s.aggregate.hfd_mad_px.toFixed(2)} px`:"—"})]})]}),e.jsxs("div",{className:"space-y-2",children:[e.jsxs("div",{className:"flex items-center justify-between text-[11px] text-on-surface-variant",children:[e.jsx("span",{children:a("cam.focus.history")}),e.jsx("span",{children:a("cam.focus.clickHint")})]}),e.jsx(Xr,{samples:_e,emptyLabel:a("cam.focus.historyEmpty")}),Ht?e.jsx("p",{className:"rounded-lg border border-error/35 bg-error-container/15 px-3 py-2 text-xs text-error",role:"alert",children:Ht}):null,s!=null&&s.warnings.includes("saturated_stars_rejected")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.saturated")}):null,s!=null&&s.warnings.includes("target_star_not_found")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.targetNotFound")}):null,s!=null&&s.warnings.includes("undersampled_stars")?e.jsx("p",{className:"rounded-lg border border-primary/30 bg-primary-container/10 px-3 py-2 text-[11px] text-on-surface",children:a("cam.focus.warning.undersampled")}):null,s!=null&&s.warnings.includes("target_may_be_hot_pixel")?e.jsx("p",{className:"rounded-lg border border-tertiary/35 bg-tertiary-container/15 px-3 py-2 text-[11px] text-tertiary",children:a("cam.focus.warning.hotPixel")}):null,s!=null&&s.detection?e.jsx("p",{className:"text-[10px] leading-relaxed text-on-surface-variant",children:a("cam.focus.detectionSummary",{measured:s.stars_measured,detected:s.stars_detected,rejected:Object.values(s.detection.quality_rejections).reduce((t,r)=>t+r,0)})}):null]})]}):e.jsxs("div",{className:"mt-3 grid gap-2 text-[11px] text-on-surface-variant sm:grid-cols-3",children:[e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["1. ",a("cam.focus.step.start")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["2. ",a("cam.focus.step.adjust")]}),e.jsxs("div",{className:"rounded-lg border border-outline-variant/20 px-3 py-2",children:["3. ",a("cam.focus.step.lock")]})]})]}),e.jsxs("div",{className:"mt-3 grid grid-cols-1 gap-2 text-xs md:grid-cols-4",children:[e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.frameFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_preview_fps)??La).toFixed(2)}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.fpsMeasureNote")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.targetFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number(((ba=n==null?void 0:n.info)==null?void 0:ba.fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.streamPacingFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.preview_target_fps)??(i==null?void 0:i.target_preview_fps)??"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:a("cam.stats.streamPacingHint")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.captureFps"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Number((i==null?void 0:i.actual_capture_fps)??0).toFixed(2)})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.exposure"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:(i==null?void 0:i.actual_exposure_us)!=null||((ga=n==null?void 0:n.info)==null?void 0:ga.actual_exposure_us)!=null?jt((i==null?void 0:i.actual_exposure_us)??((va=n==null?void 0:n.info)==null?void 0:va.actual_exposure_us)??0):"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:(ya=n==null?void 0:n.info)!=null&&ya.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encodeMs"),": "]}),e.jsxs("span",{className:"font-mono text-on-surface",children:[Number((i==null?void 0:i.jpeg_average_encode_ms)??0).toFixed(1)," ms"]})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.encoder"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:String((i==null?void 0:i.preview_encoder)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${(i==null?void 0:i.jpeg_source_format)??"RGB888"} / fail ${(i==null?void 0:i.jpeg_encode_failures)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.consumers"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${(i==null?void 0:i.preview_consumers)??0}/${(i==null?void 0:i.analysis_consumers)??0}/${(i==null?void 0:i.recording_consumers)??0}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.cameraMemory"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:`${Math.round(Number((i==null?void 0:i.process_rss_kb)??0)/1024)} / ${Math.round(Number((i==null?void 0:i.process_swap_kb)??0)/1024)} MB`})]}),(i==null?void 0:i.throttle_reason)==="auto_exposure_long"&&e.jsx("div",{className:"rounded border border-tertiary/40 bg-tertiary-container/20 px-2 py-1.5 text-left text-tertiary",children:a("cam.stats.longExposureThrottle",{exposure:de(i.actual_exposure_us,0).toFixed(4)})}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.uptime"),": "]}),e.jsx("span",{className:"font-mono text-on-surface",children:Ie.current!=null?`${Math.max(0,Math.round((performance.now()-Ie.current)/1e3))}s`:"0s"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.system.sensor"),": "]}),e.jsx("span",{className:"font-mono",children:String(((ja=n==null?void 0:n.info)==null?void 0:ja.sensor)??"—")})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.driver"),": "]}),e.jsx("span",{className:"font-mono",children:String(((wa=n==null?void 0:n.info)==null?void 0:wa.driver)??(i==null?void 0:i.camera_driver)??"—")}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:`${((Na=n==null?void 0:n.info)==null?void 0:Na.backend)??(i==null?void 0:i.camera_backend)??"—"} · lores ${(Sa=n==null?void 0:n.info)!=null&&Sa.lores_available||i!=null&&i.lores_available?"on":"off"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.stats.metadata"),": "]}),e.jsx("span",{className:"font-mono",children:((ka=n==null?void 0:n.info)==null?void 0:ka.lux)!=null?`${Number(n.info.lux).toFixed(1)} lux`:"—"}),e.jsx("p",{className:"mt-0.5 text-[10px] leading-tight text-on-surface-variant/90",children:((_a=n==null?void 0:n.info)==null?void 0:_a.colour_temperature)!=null?`${Math.round(Number(n.info.colour_temperature))}K`:"—"})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.controls.resolution"),": "]}),e.jsx("span",{className:"font-mono",children:`${((Ca=n==null?void 0:n.info)==null?void 0:Ca.width)??"—"}x${((Fa=n==null?void 0:n.info)==null?void 0:Fa.height)??"—"}`})]}),e.jsxs("div",{className:"rounded border border-outline-variant/20 bg-surface-container-low px-2 py-1.5 text-left",children:[e.jsxs("span",{className:"text-on-surface-variant",children:[a("cam.preview.mode"),": "]}),e.jsx("span",{className:"font-mono",children:(Ea=n==null?void 0:n.info)!=null&&Ea.auto_exposure?a("cam.controls.auto"):a("cam.controls.manual")})]})]}),e.jsxs("div",{className:"mt-3 flex flex-wrap gap-2",children:[e.jsxs("button",{type:"button",disabled:!He,onClick:()=>void mt(),className:`rounded px-3 py-2 text-sm disabled:opacity-50 ${He?"bg-primary-container text-on-primary-container":"border border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(Ma,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.starting":"cam.btn.start")]}),e.jsxs("button",{type:"button",disabled:!oa,onClick:()=>void Ka(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${oa?"border-error/60 text-error hover:bg-error/10":"border-outline-variant/40 text-on-surface-variant"}`,children:[e.jsx(zr,{className:"inline h-4 w-4"})," ",a(I?"cam.btn.stopping":"cam.btn.stop")]}),e.jsxs("button",{type:"button",disabled:!gr,onClick:()=>void Ya(),className:"rounded border border-outline-variant/40 px-3 py-2 text-sm disabled:opacity-50",children:[e.jsx(Ba,{className:"inline h-4 w-4"})," ",a(kt?"cam.btn.capturing":"cam.btn.capture")]}),e.jsxs("button",{type:"button",disabled:!vr,onClick:()=>void Za(),className:`rounded border px-3 py-2 text-sm disabled:opacity-50 ${n!=null&&n.recording?"border-error/60 bg-error/10 text-error":"border-outline-variant/40"}`,children:[e.jsx(Pa,{className:"inline h-4 w-4"})," ",Ke?a("cam.btn.recordBusy"):n!=null&&n.recording?a("cam.btn.recordStop"):a("cam.btn.recordStart")]})]})]}),e.jsxs("section",{className:"rounded-xl border border-outline-variant/20 bg-surface-container p-4 text-xs",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.files.title")}),e.jsx("div",{className:"mb-2",children:e.jsx("button",{type:"button",disabled:Kt,onClick:()=>void pt(),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.refresh")})}),Wa&&e.jsx("div",{className:"mb-2 text-on-surface-variant",children:a("cam.files.loadingInfo")}),C&&e.jsxs("div",{className:"mb-3 rounded border border-outline-variant/20 p-2",children:[e.jsxs("div",{className:"mb-2 flex items-center justify-between gap-2 font-semibold",children:[e.jsxs("div",{className:"flex min-w-0 items-center gap-1",children:[e.jsx(Ir,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",children:C.filename})]}),e.jsx("button",{type:"button",onClick:()=>ht(),className:"shrink-0 rounded border border-outline-variant/40 p-1 text-on-surface-variant hover:bg-surface-container","aria-label":a("cam.files.closeDetail"),children:e.jsx($r,{className:"h-3.5 w-3.5"})})]}),e.jsxs("div",{children:[a("cam.files.size"),": ",e.jsx("span",{className:"font-mono",children:Ia(C.size)})]}),e.jsxs("div",{children:[a("cam.files.type"),": ",e.jsx("span",{className:"font-mono",children:C.type})]}),e.jsxs("div",{children:[a("cam.files.modified"),": ",e.jsx("span",{className:"font-mono",children:new Date(C.modified).toLocaleString()})]}),C.exposure_us!=null&&e.jsxs("div",{children:[a("cam.controls.exposure"),": ",e.jsx("span",{className:"font-mono",children:jt(C.exposure_us)})]}),C.analogue_gain!=null&&e.jsxs("div",{children:[a("cam.controls.gain"),": ",e.jsx("span",{className:"font-mono",children:C.analogue_gain})]}),C.resolution&&e.jsxs("div",{children:[a("cam.controls.resolution"),": ",e.jsx("span",{className:"font-mono",children:C.resolution})]})]}),e.jsxs("div",{className:"max-h-96 space-y-2 overflow-auto",children:[fe.length===0&&!Kt&&e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.empty")}),jr.map(t=>e.jsxs("div",{className:`rounded border p-2 ${Re===t.name?"border-primary/50 bg-primary/5":"border-outline-variant/20"}`,children:[e.jsxs("div",{className:"flex items-center justify-between gap-2",children:[e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{className:"truncate font-semibold",children:t.name}),e.jsxs("div",{className:"text-on-surface-variant",children:[Ia(t.size)," | ",new Date(t.modified).toLocaleString()]})]}),e.jsx("div",{className:"shrink-0 text-on-surface-variant",children:t.type})]}),e.jsxs("div",{className:"mt-2 flex gap-2",children:[e.jsxs("button",{type:"button",onClick:()=>ur(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Tr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.download")]}),e.jsxs("button",{type:"button",onClick:()=>void dr(t.name),className:`rounded border px-2 py-1 ${Re===t.name?"border-primary text-primary":"border-outline-variant/40"}`,children:[e.jsx(Or,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.info")]}),e.jsxs("button",{type:"button",onClick:()=>void mr(t.name),className:"rounded border border-outline-variant/40 px-2 py-1",children:[e.jsx(Cr,{className:"mr-1 inline h-3.5 w-3.5"}),a("cam.files.delete")]})]})]},t.name))]}),e.jsxs("div",{className:"mt-3 flex items-center justify-between",children:[e.jsx("button",{type:"button",disabled:qe<=1,onClick:()=>$e(t=>Math.max(1,t-1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.prev")}),e.jsx("div",{className:"text-on-surface-variant",children:a("cam.files.page",{current:qe,total:Le})}),e.jsx("button",{type:"button",disabled:qe>=Le,onClick:()=>$e(t=>Math.min(Le,t+1)),className:"rounded border border-outline-variant/40 px-2 py-1 disabled:opacity-50",children:a("cam.files.next")})]})]})]}),e.jsxs("section",{className:"col-span-12 grid grid-cols-12 gap-4 2xl:col-span-3 2xl:self-start",children:[e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("h2",{className:"mb-3 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.title")}),e.jsxs("div",{className:"space-y-3 text-xs",children:[e.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.sensorFps"),e.jsx("input",{value:Ye,onChange:t=>{Ct(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.previewFps"),e.jsx("input",{value:Ft,onChange:t=>{Et(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]})]}),e.jsx("div",{className:"grid grid-cols-1 gap-2",children:e.jsxs("label",{className:"block",children:[a("cam.controls.sampling"),e.jsxs("select",{value:Bt,onChange:t=>{Pt(t.target.value),te(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"supersample",children:"supersample"}),e.jsx("option",{value:"native",children:"native"}),e.jsx("option",{value:"crop",children:"crop"})]})]})}),e.jsxs("label",{className:"block",children:[a("cam.controls.resolution"),e.jsx("div",{className:"mt-1 grid grid-cols-2 gap-1",children:Ur.map(t=>e.jsx("button",{type:"button",onClick:()=>{$t(t),te(!0)},className:`rounded border px-2 py-1 ${Rt===t?"border-primary text-primary":"border-outline-variant/30"}`,children:t},t))})]}),e.jsx("button",{type:"button",disabled:!xe,onClick:()=>void Qa(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applyRuntime")})]})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsxs("div",{className:"mb-2 flex items-center gap-1 text-sm font-semibold uppercase tracking-wider",children:[e.jsx(Gr,{className:"h-3.5 w-3.5"})," ",a("cam.controls.core")]}),Ee&&e.jsx("div",{className:"mb-2 rounded border border-primary/40 bg-primary/10 px-2 py-1 text-[11px] text-primary",children:a("cam.controls.pendingChanges")}),Oe&&e.jsx("p",{className:"mb-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByAe")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsx(Jr,{label:a("cam.controls.exposure"),value:d.exposureSeconds,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,exposureSeconds:t}))}}),e.jsx(X,{label:a("cam.controls.gain"),value:d.gain,min:1,max:24,step:.1,disabled:Oe,onChange:t=>{w(!0),S(r=>({...r,gain:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.digitalGain"),value:d.digitalGain,min:1,max:8,step:.1,disabled:Oe||!ra,onChange:t=>{w(!0),S(r=>({...r,digitalGain:Number(t.toFixed(1))}))}}),e.jsxs("label",{className:"block",children:[a("cam.controls.noiseReductionMode"),e.jsx("select",{value:d.noiseReductionMode,onChange:t=>{w(!0),S(r=>({...r,noiseReductionMode:t.target.value}))},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:br.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.nr.${t}`)},t))})]}),e.jsx(X,{label:a("cam.controls.contrast"),value:d.contrast,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,contrast:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.brightness"),value:d.brightness,min:-1,max:1,step:.1,onChange:t=>{w(!0),S(r=>({...r,brightness:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.saturation"),value:d.saturation,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,saturation:Number(t.toFixed(1))}))}}),e.jsx(X,{label:a("cam.controls.sharpness"),value:d.sharpness,min:0,max:2,step:.1,onChange:t=>{w(!0),S(r=>({...r,sharpness:Number(t.toFixed(1))}))}})]}),aa!=null&&e.jsx("p",{className:"mt-2 text-[11px] text-tertiary",children:a("cam.controls.manualExposureFpsLimit",{exposure:d.exposureSeconds.toFixed(4),fps:aa.toFixed(2)})}),!ra&&e.jsxs("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:[a("cam.controls.digitalGainReadOnly"),": ",e.jsx("span",{className:"font-mono",children:Number(((Ra=n==null?void 0:n.info)==null?void 0:Ra.actual_digital_gain)??d.digitalGain).toFixed(2)})]}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",disabled:!Ee,onClick:()=>void er(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5 disabled:opacity-50",children:a("cam.controls.applySettings")})})]}),e.jsxs("section",{className:"col-span-12 rounded-xl border border-outline-variant/20 bg-surface-container p-4",children:[e.jsx("div",{className:"mb-2 text-sm font-semibold uppercase tracking-wider",children:a("cam.controls.mode")}),e.jsxs("div",{className:"grid grid-cols-2 gap-2 text-xs",children:[e.jsxs("label",{className:"block",children:[a("cam.controls.autoExposure"),e.jsxs("select",{value:d.autoExposure?"auto":"manual",onChange:t=>{S(r=>({...r,autoExposure:t.target.value==="auto"})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"auto",children:a("cam.controls.auto")}),e.jsx("option",{value:"manual",children:a("cam.controls.manual")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.colorMode"),e.jsxs("select",{value:d.colorMode,onChange:t=>{S(r=>({...r,colorMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"color",children:a("cam.controls.color")}),e.jsx("option",{value:"mono",children:a("cam.controls.mono")})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.whiteBalance"),e.jsx("select",{value:d.whiteBalanceMode,onChange:t=>{S(r=>({...r,whiteBalanceMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:fr.map(t=>e.jsx("option",{value:t,children:a(`cam.controls.wb.${t}`)},t))})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.aeFlicker"),e.jsxs("select",{value:d.aeFlickerMode,onChange:t=>{S(r=>({...r,aeFlickerMode:t.target.value})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5",children:[e.jsx("option",{value:"off",children:a("cam.controls.off")}),e.jsx("option",{value:"50hz",children:"50 Hz"}),e.jsx("option",{value:"60hz",children:"60 Hz"})]})]}),e.jsxs("label",{className:"block",children:[a("cam.controls.maxAeFrame"),e.jsx("input",{type:"number",min:.01,max:.6,step:.01,value:d.autoExposureMaxSeconds,onChange:t=>{S(r=>({...r,autoExposureMaxSeconds:N(Number(t.target.value)||.6,.01,.6)})),w(!0)},className:"mt-1 w-full rounded border border-outline-variant/30 bg-surface-container-low px-2 py-1.5"})]}),e.jsx(X,{label:"R Gain",value:d.whiteBalanceGainR,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainR:Number(t.toFixed(1))})),w(!0)}}),e.jsx(X,{label:"B Gain",value:d.whiteBalanceGainB,min:.1,max:3,step:.1,disabled:!ft,onChange:t=>{S(r=>({...r,whiteBalanceGainB:Number(t.toFixed(1))})),w(!0)}})]}),!ft&&e.jsx("p",{className:"mt-2 text-[11px] text-on-surface-variant",children:a("cam.controls.lockedByWb")}),e.jsx("div",{className:"mt-2",children:e.jsx("button",{type:"button",onClick:()=>void tr(),className:"w-full rounded border border-outline-variant/40 px-2 py-1.5",children:a("cam.controls.applyMode")})})]})]})]})]}),(ue||me)&&e.jsxs("div",{className:"fixed bottom-4 right-4 z-40 max-w-md space-y-2 text-xs",children:[ue&&e.jsx("div",{className:"rounded border border-error/40 bg-error-container/20 px-3 py-2 text-on-error-container",children:ue}),me&&e.jsx("div",{className:"rounded border border-primary/30 bg-primary/10 px-3 py-2 text-on-surface",children:me})]})]})}Sr.createRoot(document.getElementById("root")).render(e.jsx(kr.StrictMode,{children:e.jsx(Fr,{children:e.jsx(Pr,{children:e.jsx(Kr,{})})})})); diff --git a/web/static/analysis-lab/camera.html b/web/static/analysis-lab/camera.html index 21fc380..794148c 100644 --- a/web/static/analysis-lab/camera.html +++ b/web/static/analysis-lab/camera.html @@ -4,7 +4,7 @@ OGScope 相机调试控制台 - + From 1e088b1b607eb7c819d48fa7ad1332a92e10deca Mon Sep 17 00:00:00 2001 From: "Richard.Lu" Date: Tue, 1 Sep 2026 19:48:42 +0800 Subject: [PATCH 21/24] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20MJPEG=20?= =?UTF-8?q?=E5=81=9C=E6=BB=9E=E8=BF=9E=E6=8E=A5=E8=B5=84=E6=BA=90=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=20/=20fix=20stalled=20MJPEG=20session=20leaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/ogscope.env.example | 1 + docs/development/ogscope-service-hardening.md | 1 + ogscope/config.py | 15 +- ogscope/config_catalog.py | 1 + ogscope/domain/camera/services.py | 15 +- ogscope/domain/camera/stream_limiter.py | 101 ++++++++-- ogscope/domain/camera/streaming.py | 178 +++++++++++++++++- ogscope/web/api/models/schemas.py | 6 + tests/unit/test_domain_camera_streaming.py | 125 +++++++++--- tests/unit/test_mjpeg_stream_limiter.py | 64 ++++--- 10 files changed, 429 insertions(+), 78 deletions(-) diff --git a/deploy/ogscope.env.example b/deploy/ogscope.env.example index d4839b9..6ee867c 100644 --- a/deploy/ogscope.env.example +++ b/deploy/ogscope.env.example @@ -57,6 +57,7 @@ OGSCOPE_RELOAD=false # OGSCOPE_KEEP_RAW_CACHE=false # OGSCOPE_STREAM_MAX_MJPEG_CLIENTS=4 # OGSCOPE_STREAM_MJPEG_FRAME_FETCH_TIMEOUT_MS=20000 +# OGSCOPE_STREAM_MJPEG_CLIENT_STALL_TIMEOUT_MS=30000 # --- SPI 屏幕 / SPI display --- # OGSCOPE_DISPLAY_ENABLED=false diff --git a/docs/development/ogscope-service-hardening.md b/docs/development/ogscope-service-hardening.md index 3b55c7b..8189659 100644 --- a/docs/development/ogscope-service-hardening.md +++ b/docs/development/ogscope-service-hardening.md @@ -32,6 +32,7 @@ sudo dmesg -T | grep -i -E 'oom|killed process' |-----------------|----------------| | `OGSCOPE_PREVIEW_JPEG_QUALITY` | 共享预览 JPEG 质量(与调试 MJPEG 默认质量一致)/ Shared preview JPEG quality | | `OGSCOPE_SHARED_PREVIEW_FPS` | 共享抓帧与 MJPEG 推送目标帧率 / Shared grabber and MJPEG pacing FPS | +| `OGSCOPE_STREAM_MJPEG_CLIENT_STALL_TIMEOUT_MS` | 下游 MJPEG 发送无进展超时,0 表示禁用 / Downstream MJPEG send-stall timeout; 0 disables it | | `OGSCOPE_DEBUG_PREVIEW_MIN_INTERVAL_MS` | 调试「单帧预览」接口每客户端最小间隔(毫秒);过短返回 304 / Min interval for `/api/dev/debug/camera/preview` per client | | `OGSCOPE_CAMERA_CAPTURE_TIMEOUT_SEC` | 单次 Picamera2 抓帧的硬超时,必须高于最长曝光;超时后复用未完成任务,不重复排队新请求 / Hard timeout for one Picamera2 capture; keep above maximum exposure and do not queue duplicate jobs after timeout | | `OGSCOPE_KEEP_RAW_CACHE` | `1` 时在共享管理器中常驻 `_latest_raw`;默认 `0` 以省内存 / Retain raw frame cache when `1` | diff --git a/ogscope/config.py b/ogscope/config.py index d3e72f3..e00dbad 100644 --- a/ogscope/config.py +++ b/ogscope/config.py @@ -349,9 +349,18 @@ class Settings(BaseSettings): ge=3000, le=120000, description=( - "MJPEG 循环单次取帧(含编码线程)最大等待毫秒;超时则结束流并释放名额," - "避免异常断开时长时间占满并发 / Max wait per MJPEG frame fetch (incl. encode thread); " - "on timeout the stream ends to free slots after abnormal client disconnect" + "MJPEG 循环单次取帧(含编码线程)最大等待毫秒;超时表示相机取帧停滞 / " + "Max wait per MJPEG frame fetch (incl. encode thread); timeout means camera fetch stalled" + ), + ) + stream_mjpeg_client_stall_timeout_ms: int = Field( + default=30000, + ge=0, + le=300000, + description=( + "MJPEG 下游发送无进展的最大毫秒数;至少比取帧超时多 5 秒,0=禁用 / " + "Max time without downstream MJPEG send progress; effectively at least 5s above " + "frame-fetch timeout; 0=disabled" ), ) diff --git a/ogscope/config_catalog.py b/ogscope/config_catalog.py index 60a4115..aa0f8ab 100644 --- a/ogscope/config_catalog.py +++ b/ogscope/config_catalog.py @@ -103,6 +103,7 @@ "keep_raw_cache", "stream_max_mjpeg_clients", "stream_mjpeg_frame_fetch_timeout_ms", + "stream_mjpeg_client_stall_timeout_ms", ), ), ( diff --git a/ogscope/domain/camera/services.py b/ogscope/domain/camera/services.py index 16f7416..1010cf3 100644 --- a/ogscope/domain/camera/services.py +++ b/ogscope/domain/camera/services.py @@ -163,11 +163,24 @@ async def get_stream_status(self) -> dict[str, Any]: from ogscope.web.camera_shared import get_camera_manager metrics = await get_camera_manager().stream_metrics() + limiter_metrics = await limiter.snapshot() + configured_stall_timeout_ms = int( + settings.stream_mjpeg_client_stall_timeout_ms + ) + effective_stall_timeout_ms = ( + max( + configured_stall_timeout_ms, + int(settings.stream_mjpeg_frame_fetch_timeout_ms) + 5000, + ) + if configured_stall_timeout_ms > 0 + else 0 + ) return { "max_clients": int(limiter.max_clients), - "active_clients": int(limiter.active_clients), "frame_fetch_timeout_ms": int(settings.stream_mjpeg_frame_fetch_timeout_ms), + "client_stall_timeout_ms": effective_stall_timeout_ms, "target_preview_fps": int(metrics["preview_target_fps"]), + **limiter_metrics, **metrics, } diff --git a/ogscope/domain/camera/stream_limiter.py b/ogscope/domain/camera/stream_limiter.py index a952f48..4a87109 100644 --- a/ogscope/domain/camera/stream_limiter.py +++ b/ogscope/domain/camera/stream_limiter.py @@ -1,20 +1,53 @@ """ -MJPEG 长连接并发限制 / Concurrent MJPEG stream limiter +MJPEG 长连接会话限制 / Concurrent MJPEG stream session limiter. """ from __future__ import annotations import asyncio +import time +from collections import Counter +from dataclasses import dataclass from ogscope.config import get_settings +@dataclass +class _MjpegSessionState: + """保存单个流会话的活跃时间 / Track timing for one stream session.""" + + acquired_mono: float + last_progress_mono: float + + +class MjpegStreamLease: + """可幂等释放的 MJPEG 名额租约 / Idempotently releasable MJPEG slot lease.""" + + def __init__(self, limiter: MjpegStreamLimiter, session_id: int) -> None: + self._limiter = limiter + self._session_id = session_id + + async def touch(self) -> bool: + """记录一次已完成的下游发送 / Record one completed downstream send.""" + return await self._limiter._touch(self._session_id) + + async def idle_seconds(self) -> float | None: + """返回距最近发送进展的秒数 / Return seconds since the latest send progress.""" + return await self._limiter._idle_seconds(self._session_id) + + async def release(self, reason: str = "released") -> bool: + """幂等释放租约并记录原因 / Idempotently release the lease and record its reason.""" + return await self._limiter._release(self._session_id, reason) + + class MjpegStreamLimiter: - """限制同时活跃的 MJPEG 响应数,减轻低配板内存与 WiFi 压力 / Cap concurrent MJPEG responses.""" + """限制并跟踪 MJPEG 响应,防止失联客户端永久占位 / Limit and track MJPEG responses.""" def __init__(self, max_clients: int) -> None: self._max = max(0, int(max_clients)) - self._count = 0 + self._next_session_id = 1 + self._sessions: dict[int, _MjpegSessionState] = {} + self._release_reasons: Counter[str] = Counter() self._lock = asyncio.Lock() @property @@ -23,24 +56,62 @@ def max_clients(self) -> int: @property def active_clients(self) -> int: - return self._count + return len(self._sessions) - async def try_acquire(self) -> bool: - """若未超限则占用一个名额并返回 True / Acquire one slot if under limit.""" - if self._max <= 0: - return True + async def try_acquire(self) -> MjpegStreamLease | None: + """若未超限则返回会话租约 / Return a session lease when under the limit.""" + async with self._lock: + if self._max > 0 and len(self._sessions) >= self._max: + return None + session_id = self._next_session_id + self._next_session_id += 1 + now = time.monotonic() + self._sessions[session_id] = _MjpegSessionState(now, now) + return MjpegStreamLease(self, session_id) + + async def snapshot(self) -> dict[str, object]: + """生成无敏感标识的会话指标 / Build session metrics without client identifiers.""" + async with self._lock: + now = time.monotonic() + states = list(self._sessions.values()) + return { + "active_clients": len(states), + "oldest_client_age_ms": int( + max((now - state.acquired_mono for state in states), default=0.0) + * 1000 + ), + "oldest_client_idle_ms": int( + max((now - state.last_progress_mono for state in states), default=0.0) + * 1000 + ), + "released_clients_total": sum(self._release_reasons.values()), + "stalled_clients_total": self._release_reasons.get( + "client_stall_timeout", 0 + ), + "release_reasons": dict(self._release_reasons), + } + + async def _touch(self, session_id: int) -> bool: async with self._lock: - if self._count >= self._max: + state = self._sessions.get(session_id) + if state is None: return False - self._count += 1 + state.last_progress_mono = time.monotonic() return True - async def release(self) -> None: - """释放一个名额(与 try_acquire 成对)/ Release one slot.""" - if self._max <= 0: - return + async def _idle_seconds(self, session_id: int) -> float | None: async with self._lock: - self._count = max(0, self._count - 1) + state = self._sessions.get(session_id) + if state is None: + return None + return max(0.0, time.monotonic() - state.last_progress_mono) + + async def _release(self, session_id: int, reason: str) -> bool: + async with self._lock: + if self._sessions.pop(session_id, None) is None: + return False + self._release_reasons[reason or "released"] += 1 + return True _limiter: MjpegStreamLimiter | None = None diff --git a/ogscope/domain/camera/streaming.py b/ogscope/domain/camera/streaming.py index 28036d3..f424e09 100644 --- a/ogscope/domain/camera/streaming.py +++ b/ogscope/domain/camera/streaming.py @@ -7,18 +7,140 @@ import asyncio import logging import time +from collections.abc import Coroutine +from typing import Any from fastapi import HTTPException from fastapi.responses import StreamingResponse +from starlette.background import BackgroundTask from starlette.requests import Request from ogscope.config import get_settings from ogscope.domain.camera.services import camera_domain_service -from ogscope.domain.camera.stream_limiter import get_mjpeg_stream_limiter +from ogscope.domain.camera.stream_limiter import ( + MjpegStreamLease, + get_mjpeg_stream_limiter, +) from ogscope.web.camera_shared import get_camera_manager from ogscope.web.mjpeg_stream_helpers import mjpeg_sleep_or_disconnect +_cleanup_tasks: set[asyncio.Task[Any]] = set() +_module_logger = logging.getLogger(__name__) + + +def _finish_cleanup_task(task: asyncio.Task[Any]) -> None: + """移除并读取清理结果,避免遗失后台异常 / Remove the task and consume background errors.""" + _cleanup_tasks.discard(task) + if task.cancelled(): + return + exc = task.exception() + if exc is not None: + _module_logger.error( + "mjpeg_cleanup_failed error=%s", + exc, + exc_info=(type(exc), exc, exc.__traceback__), + ) + + +def _retain_cleanup_task(coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]: + """保留清理任务直至完成,避免调用方取消中断资源回收 / Retain cleanup across caller cancellation.""" + task = asyncio.create_task(coro) + _cleanup_tasks.add(task) + task.add_done_callback(_finish_cleanup_task) + return task + + +class _MjpegStreamSession: + """统一持有流租约与相机消费者 / Own the stream lease and camera consumer together.""" + + def __init__( + self, + *, + lease: MjpegStreamLease, + manager: Any, + stall_timeout_s: float, + logger: logging.Logger, + path: str, + ) -> None: + self._lease = lease + self._manager = manager + self._stall_timeout_s = max(0.0, stall_timeout_s) + self._logger = logger + self._path = path + self._preview_acquired = False + self._closed = False + self._cleanup_task: asyncio.Task[Any] | None = None + self._watchdog_task: asyncio.Task[Any] | None = None + + @property + def closed(self) -> bool: + return self._closed + + def start_watchdog(self) -> None: + """在响应开始前启动发送停滞看门狗 / Start the send-stall watchdog before response iteration.""" + if self._stall_timeout_s > 0 and self._watchdog_task is None: + self._watchdog_task = asyncio.create_task(self._watch_for_stall()) + + async def acquire_preview_consumer(self) -> bool: + """获取相机消费者,并处理获取期间发生的超时 / Acquire camera consumer safely across timeout.""" + await self._manager.acquire_preview_consumer() + if self._closed: + task = _retain_cleanup_task(self._manager.release_preview_consumer()) + await asyncio.shield(task) + return False + self._preview_acquired = True + return True + + async def touch(self) -> None: + """仅在上一帧已完成 ASGI 发送后刷新进展 / Refresh progress only after the prior ASGI send completed.""" + await self._lease.touch() + + async def close(self, reason: str = "response_complete") -> None: + """幂等关闭;独立清理任务可跨越调用方取消 / Close idempotently in a cancellation-safe task.""" + if self._cleanup_task is None: + self._closed = True + self._cleanup_task = _retain_cleanup_task(self._close_once(reason)) + await asyncio.shield(self._cleanup_task) + + async def _close_once(self, reason: str) -> None: + watchdog = self._watchdog_task + current = asyncio.current_task() + if watchdog is not None and watchdog is not current: + watchdog.cancel() + + # 先释放并发租约,再等待相机停止 / Release the slot before awaiting camera shutdown. + released = await self._lease.release(reason) + if released: + self._logger.info( + "mjpeg_session_released reason=%s path=%s", reason, self._path + ) + + if self._preview_acquired: + self._preview_acquired = False + await self._manager.release_preview_consumer() + + async def _watch_for_stall(self) -> None: + interval = min(1.0, max(0.05, self._stall_timeout_s / 4.0)) + try: + while not self._closed: + await asyncio.sleep(interval) + idle_s = await self._lease.idle_seconds() + if idle_s is None: + return + if idle_s >= self._stall_timeout_s: + self._logger.warning( + "mjpeg_client_stall_timeout idle_ms=%s timeout_ms=%s path=%s", + int(idle_s * 1000), + int(self._stall_timeout_s * 1000), + self._path, + ) + await self.close("client_stall_timeout") + return + except asyncio.CancelledError: + return + + async def build_camera_mjpeg_stream( request: Request, *, @@ -30,28 +152,52 @@ async def build_camera_mjpeg_stream( ) -> StreamingResponse: """构建 MJPEG 流响应 / Build MJPEG stream response.""" limiter = get_mjpeg_stream_limiter() - if not await limiter.try_acquire(): - _path = str(getattr(getattr(request, "url", None), "path", "") or "") + lease = await limiter.try_acquire() + if lease is None: + path = str(getattr(getattr(request, "url", None), "path", "") or "") logger.warning( "mjpeg_try_acquire_rejected active=%s max=%s path=%s", limiter.active_clients, limiter.max_clients, - _path, + path, ) raise HTTPException(status_code=503, detail=limit_detail) + boundary = "frame" settings = get_settings() fetch_timeout_s = settings.stream_mjpeg_frame_fetch_timeout_ms / 1000.0 + configured_stall_timeout_ms = settings.stream_mjpeg_client_stall_timeout_ms + stall_timeout_s = ( + max( + configured_stall_timeout_ms, + settings.stream_mjpeg_frame_fetch_timeout_ms + 5000, + ) + / 1000.0 + if configured_stall_timeout_ms > 0 + else 0.0 + ) content_type = "image/jpeg" if image_format.lower() == "jpeg" else "image/png" + path = str(getattr(getattr(request, "url", None), "path", "") or "") + manager = get_camera_manager() + session = _MjpegStreamSession( + lease=lease, + manager=manager, + stall_timeout_s=stall_timeout_s, + logger=logger, + path=path, + ) + session.start_watchdog() async def frame_generator(): - manager = get_camera_manager() + release_reason = "response_complete" try: - await manager.acquire_preview_consumer() + if not await session.acquire_preview_consumer(): + return last_snap_frame_id = -1 last_emit_mono = 0.0 - while True: + while not session.closed: if await request.is_disconnected(): + release_reason = "client_disconnect" break try: code, data, snap_id = await asyncio.wait_for( @@ -61,20 +207,24 @@ async def frame_generator(): timeout=fetch_timeout_s, ) except asyncio.TimeoutError: + release_reason = "frame_fetch_timeout" logger.warning(timeout_log_message) break if code == 304: if not await mjpeg_sleep_or_disconnect(request, 0.03): + release_reason = "client_disconnect" break continue if code != 200 or data is None: if not await mjpeg_sleep_or_disconnect(request, 0.05): + release_reason = "client_disconnect" break continue now = time.monotonic() min_emit_interval = 1.0 / max(1, manager.preview_target_fps) wait = last_emit_mono + min_emit_interval - now if wait > 0 and not await mjpeg_sleep_or_disconnect(request, wait): + release_reason = "client_disconnect" break last_snap_frame_id = snap_id last_emit_mono = time.monotonic() @@ -91,9 +241,18 @@ async def frame_generator(): + data + b"\r\n" ) + await session.touch() + except asyncio.CancelledError: + release_reason = "cancelled" + raise + except GeneratorExit: + release_reason = "generator_closed" + raise + except Exception: + release_reason = "stream_error" + raise finally: - await manager.release_preview_consumer() - await limiter.release() + await session.close(release_reason) return StreamingResponse( frame_generator(), @@ -102,4 +261,5 @@ async def frame_generator(): "Cache-Control": "no-store, no-cache, must-revalidate", "X-Accel-Buffering": "no", }, + background=BackgroundTask(session.close, "response_complete"), ) diff --git a/ogscope/web/api/models/schemas.py b/ogscope/web/api/models/schemas.py index 1c8c55b..d6da659 100644 --- a/ogscope/web/api/models/schemas.py +++ b/ogscope/web/api/models/schemas.py @@ -568,7 +568,13 @@ class CoreStreamStatusResponse(BaseModel): max_clients: int active_clients: int frame_fetch_timeout_ms: int + client_stall_timeout_ms: int = 0 target_preview_fps: int + oldest_client_age_ms: int = 0 + oldest_client_idle_ms: int = 0 + released_clients_total: int = 0 + stalled_clients_total: int = 0 + release_reasons: dict[str, int] = Field(default_factory=dict) sensor_target_fps: float = 0.0 preview_target_fps: int = 0 actual_capture_fps: float = 0.0 diff --git a/tests/unit/test_domain_camera_streaming.py b/tests/unit/test_domain_camera_streaming.py index 18e3e1c..d081b1e 100644 --- a/tests/unit/test_domain_camera_streaming.py +++ b/tests/unit/test_domain_camera_streaming.py @@ -1,9 +1,14 @@ +"""MJPEG 流生命周期测试 / MJPEG stream lifecycle tests.""" + from __future__ import annotations +import asyncio + import pytest from fastapi import HTTPException from ogscope.domain.camera import streaming as streaming_mod +from ogscope.domain.camera.stream_limiter import MjpegStreamLimiter class _FakeRequest: @@ -19,17 +24,21 @@ async def is_disconnected(self) -> bool: return value -class _FakeLimiter: - def __init__(self, can_acquire: bool = True) -> None: - self.max_clients = 2 - self.active_clients = 0 - self._can_acquire = can_acquire +class _FakeSettings: + stream_mjpeg_frame_fetch_timeout_ms = 1000 + stream_mjpeg_client_stall_timeout_ms = 1000 + + +class _FakeManager: + def __init__(self) -> None: + self.acquired = False self.released = False + self.preview_target_fps = 8 - async def try_acquire(self) -> bool: - return self._can_acquire + async def acquire_preview_consumer(self) -> None: + self.acquired = True - async def release(self) -> None: + async def release_preview_consumer(self) -> None: self.released = True @@ -38,7 +47,9 @@ async def release(self) -> None: async def test_build_camera_mjpeg_stream_rejects_when_limit_reached( monkeypatch, ) -> None: - limiter = _FakeLimiter(can_acquire=False) + limiter = MjpegStreamLimiter(1) + held_lease = await limiter.try_acquire() + assert held_lease is not None monkeypatch.setattr(streaming_mod, "get_mjpeg_stream_limiter", lambda: limiter) req = _FakeRequest([False]) @@ -52,31 +63,16 @@ async def test_build_camera_mjpeg_stream_rejects_when_limit_reached( logger=streaming_mod.logging.getLogger(__name__), ) assert exc.value.status_code == 503 + await held_lease.release() @pytest.mark.unit @pytest.mark.asyncio async def test_build_camera_mjpeg_stream_yields_frame_and_releases(monkeypatch) -> None: - limiter = _FakeLimiter(can_acquire=True) + limiter = MjpegStreamLimiter(2) monkeypatch.setattr(streaming_mod, "get_mjpeg_stream_limiter", lambda: limiter) - - class _FakeSettings: - stream_mjpeg_frame_fetch_timeout_ms = 1000 - shared_preview_fps = 8 - monkeypatch.setattr(streaming_mod, "get_settings", lambda: _FakeSettings()) - class _FakeManager: - acquired = False - released = False - preview_target_fps = 8 - - async def acquire_preview_consumer(self) -> None: - self.acquired = True - - async def release_preview_consumer(self) -> None: - self.released = True - manager = _FakeManager() monkeypatch.setattr(streaming_mod, "get_camera_manager", lambda: manager) @@ -92,9 +88,8 @@ async def _fake_get_stream_frame_bytes( _fake_get_stream_frame_bytes, ) - req = _FakeRequest([False, True]) resp = await streaming_mod.build_camera_mjpeg_stream( - req, + _FakeRequest([False, True]), image_format="jpeg", quality=75, limit_detail="limit", @@ -105,6 +100,78 @@ async def _fake_get_stream_frame_bytes( first_chunk = await anext(body_iter) assert b"Content-Type: image/jpeg" in first_chunk await body_iter.aclose() - assert limiter.released is True + + assert limiter.active_clients == 0 assert manager.acquired is True assert manager.released is True + snapshot = await limiter.snapshot() + assert snapshot["release_reasons"] == {"generator_closed": 1} + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_session_releases_slot_before_slow_camera_cleanup() -> None: + limiter = MjpegStreamLimiter(1) + lease = await limiter.try_acquire() + assert lease is not None + cleanup_started = asyncio.Event() + allow_cleanup = asyncio.Event() + + class _SlowReleaseManager(_FakeManager): + async def release_preview_consumer(self) -> None: + cleanup_started.set() + await allow_cleanup.wait() + self.released = True + + manager = _SlowReleaseManager() + session = streaming_mod._MjpegStreamSession( + lease=lease, + manager=manager, + stall_timeout_s=0, + logger=streaming_mod.logging.getLogger(__name__), + path="/test", + ) + assert await session.acquire_preview_consumer() is True + + close_caller = asyncio.create_task(session.close("cancelled")) + await cleanup_started.wait() + assert limiter.active_clients == 0 + close_caller.cancel() + with pytest.raises(asyncio.CancelledError): + await close_caller + + allow_cleanup.set() + for _ in range(20): + if manager.released: + break + await asyncio.sleep(0.01) + assert manager.released is True + + +@pytest.mark.unit +@pytest.mark.asyncio +async def test_session_watchdog_reclaims_stalled_client() -> None: + limiter = MjpegStreamLimiter(1) + lease = await limiter.try_acquire() + assert lease is not None + manager = _FakeManager() + session = streaming_mod._MjpegStreamSession( + lease=lease, + manager=manager, + stall_timeout_s=0.05, + logger=streaming_mod.logging.getLogger(__name__), + path="/test", + ) + assert await session.acquire_preview_consumer() is True + session.start_watchdog() + + for _ in range(30): + if limiter.active_clients == 0 and manager.released: + break + await asyncio.sleep(0.01) + + assert limiter.active_clients == 0 + assert manager.released is True + snapshot = await limiter.snapshot() + assert snapshot["stalled_clients_total"] == 1 + await session.close("duplicate") diff --git a/tests/unit/test_mjpeg_stream_limiter.py b/tests/unit/test_mjpeg_stream_limiter.py index f51de15..259cc66 100644 --- a/tests/unit/test_mjpeg_stream_limiter.py +++ b/tests/unit/test_mjpeg_stream_limiter.py @@ -1,38 +1,60 @@ -"""MJPEG 并发限制单元测试 / Unit tests for MJPEG stream limiter.""" +"""MJPEG 会话限制单元测试 / Unit tests for MJPEG stream session limiter.""" from __future__ import annotations +import asyncio + import pytest from ogscope.web.mjpeg_stream_limiter import MjpegStreamLimiter @pytest.mark.asyncio -async def test_limiter_unlimited_always_acquires() -> None: +async def test_limiter_unlimited_tracks_and_releases_leases() -> None: lim = MjpegStreamLimiter(0) - assert await lim.try_acquire() is True - assert await lim.try_acquire() is True - await lim.release() - await lim.release() + first = await lim.try_acquire() + second = await lim.try_acquire() + assert first is not None + assert second is not None + assert lim.active_clients == 2 + + assert await first.release("client_disconnect") is True + assert await first.release("duplicate") is False + assert await second.release("response_complete") is True + assert lim.active_clients == 0 + + snapshot = await lim.snapshot() + assert snapshot["released_clients_total"] == 2 + assert snapshot["release_reasons"] == { + "client_disconnect": 1, + "response_complete": 1, + } @pytest.mark.asyncio -async def test_limiter_one_client_blocks_second() -> None: +async def test_limiter_one_client_blocks_second_until_lease_release() -> None: lim = MjpegStreamLimiter(1) - assert await lim.try_acquire() is True - assert await lim.try_acquire() is False - await lim.release() - assert await lim.try_acquire() is True - await lim.release() + lease = await lim.try_acquire() + assert lease is not None + assert await lim.try_acquire() is None + assert await lease.release() is True + replacement = await lim.try_acquire() + assert replacement is not None + await replacement.release() @pytest.mark.asyncio -async def test_limiter_two_clients_blocks_third() -> None: - lim = MjpegStreamLimiter(2) - assert await lim.try_acquire() is True - assert await lim.try_acquire() is True - assert await lim.try_acquire() is False - await lim.release() - assert await lim.try_acquire() is True - await lim.release() - await lim.release() +async def test_limiter_touch_updates_idle_metric() -> None: + lim = MjpegStreamLimiter(1) + lease = await lim.try_acquire() + assert lease is not None + await asyncio.sleep(0.02) + before = await lim.snapshot() + assert before["oldest_client_idle_ms"] >= 10 + + assert await lease.touch() is True + after = await lim.snapshot() + assert after["oldest_client_idle_ms"] < before["oldest_client_idle_ms"] + await lease.release("client_stall_timeout") + final = await lim.snapshot() + assert final["stalled_clients_total"] == 1 From c7e2a98d92360b3cdc8c0b9d77f80b09bf24240a Mon Sep 17 00:00:00 2001 From: "Richard.Lu" Date: Tue, 1 Sep 2026 20:34:16 +0800 Subject: [PATCH 22/24] =?UTF-8?q?chore(observability):=20=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=A7=A3=E7=AE=97=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E5=B9=B6=E9=99=8D=E4=BD=8E=E6=97=A5=E5=BF=97=E5=99=AA=E5=A3=B0?= =?UTF-8?q?=20/=20trace=20solve=20lifecycle=20and=20reduce=20log=20noise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ogscope/core/application/core_service.py | 4 +- ogscope/core/realtime/service.py | 66 +++++++++++++++++++++- ogscope/main.py | 3 + ogscope/web/app.py | 22 ++++++++ tests/unit/test_core_contract_api.py | 1 + tests/unit/test_realtime_solve_pipeline.py | 20 ++++++- 6 files changed, 112 insertions(+), 4 deletions(-) diff --git a/ogscope/core/application/core_service.py b/ogscope/core/application/core_service.py index a3927b1..8cdab0d 100644 --- a/ogscope/core/application/core_service.py +++ b/ogscope/core/application/core_service.py @@ -192,6 +192,7 @@ async def start_analysis( ) -> dict[str, Any]: """开始实时分析 / Start realtime analysis.""" was_running = self._session.running + next_session_id = self._session.session_id if was_running else uuid.uuid4().hex result = await realtime_solve_service.start( hint_ra_deg=hint_ra_deg, hint_dec_deg=hint_dec_deg, @@ -199,13 +200,14 @@ async def start_analysis( fov_max_error=fov_max_error, solve_timeout_ms=solve_timeout_ms, solve_context=solve_context, + session_id=next_session_id, ) success = bool(result.get("success", True)) self._session.running = success if success and not was_running: # A unique id lets upstream consumers reject results from an older run. # 唯一会话 ID 让上层能够拒绝旧一轮分析留下的结果。 - self._session.session_id = uuid.uuid4().hex + self._session.session_id = str(result.get("session_id") or next_session_id) return { "success": success, "session_id": self._session.session_id, diff --git a/ogscope/core/realtime/service.py b/ogscope/core/realtime/service.py index af41e58..4d1db96 100644 --- a/ogscope/core/realtime/service.py +++ b/ogscope/core/realtime/service.py @@ -5,10 +5,13 @@ from __future__ import annotations import asyncio +import json import time from dataclasses import dataclass from typing import Any +from loguru import logger + from ogscope.algorithms.plate_solve import PlateSolver, SolveResult from ogscope.algorithms.plate_solve.sensor_context import attach_sensor_prediction from ogscope.config import effective_solver_max_stars, get_settings @@ -24,6 +27,8 @@ class RealtimeState: fullsolve_count: int = 0 last_result: dict[str, Any] | None = None last_error: str = "" + session_id: str = "" + started_mono: float = 0.0 class RealtimeSolveService: @@ -60,11 +65,13 @@ async def start( fov_max_error: float | None = None, solve_timeout_ms: int | None = None, solve_context: Any | None = None, + session_id: str | None = None, ) -> dict[str, Any]: """启动实时解算 / Start realtime solving""" if self.state.running: return { "success": True, + "session_id": self.state.session_id, "message": "实时解算已在运行 / Realtime solver already running", } if hint_ra_deg is not None: @@ -75,13 +82,28 @@ async def start( self._fov_max_error = fov_max_error self._solve_timeout_ms = solve_timeout_ms self._solve_context = solve_context - self.state = RealtimeState(running=True) + self.state = RealtimeState( + running=True, + session_id=str(session_id or ""), + started_mono=time.monotonic(), + ) self._has_fullsolve = False self._task = asyncio.create_task(self._loop()) - return {"success": True, "message": "实时解算已启动 / Realtime solver started"} + self._log_event( + "session_started", + fov_estimate=fov_estimate, + fov_max_error=fov_max_error, + solve_timeout_ms=solve_timeout_ms, + ) + return { + "success": True, + "session_id": self.state.session_id, + "message": "实时解算已启动 / Realtime solver started", + } async def stop(self) -> dict[str, Any]: """停止实时解算 / Stop realtime solving""" + was_running = self.state.running self.state.running = False if self._task and not self._task.done(): self._task.cancel() @@ -90,6 +112,14 @@ async def stop(self) -> dict[str, Any]: except asyncio.CancelledError: pass self._task = None + if was_running: + self._log_event( + "session_stopped", + frame_count=self.state.frame_count, + fullsolve_count=self.state.fullsolve_count, + last_status=str((self.state.last_result or {}).get("status") or ""), + last_error=self.state.last_error, + ) return {"success": True, "message": "实时解算已停止 / Realtime solver stopped"} async def get_status(self) -> dict[str, Any]: @@ -100,6 +130,7 @@ async def get_status(self) -> dict[str, Any]: "fullsolve_count": self.state.fullsolve_count, "last_result": self.state.last_result, "last_error": self.state.last_error, + "session_id": self.state.session_id, } async def _loop(self) -> None: @@ -142,12 +173,25 @@ async def _loop(self) -> None: != "MATCH_FOUND" ) if use_fullsolve: + solve_started = time.monotonic() solved = await asyncio.to_thread( self._solve_frame_sync, frame, ) self._apply_solve_result(solved) self.state.fullsolve_count += 1 + self._log_event( + "fullsolve_finished", + frame_count=self.state.frame_count, + fullsolve_count=self.state.fullsolve_count, + status=solved.status, + detected_stars=solved.detected_stars, + matches=solved.matches, + t_solve_ms=solved.t_solve_ms, + t_extract_ms=solved.t_extract_ms, + t_preprocess_ms=solved.t_preprocess_ms, + wall_ms=int((time.monotonic() - solve_started) * 1000), + ) # ``solve_from_bgr_frame`` is the authoritative production # image pipeline. Keep only a sentinel here; StarExtractor # remains available for focus metrics and lightweight counts. @@ -155,9 +199,27 @@ async def _loop(self) -> None: # StarExtractor 仍用于焦点指标和轻量星点计数。 self._has_fullsolve = True except Exception as exc: # noqa: BLE001 + changed = str(exc) != self.state.last_error self.state.last_error = str(exc) + if changed: + self._log_event("loop_error", error=str(exc), level="warning") await asyncio.sleep(0.1) + def _log_event(self, event: str, *, level: str = "info", **fields: Any) -> None: + """Emit one correlated solve lifecycle event / 输出一条可关联的解算生命周期事件。""" + started = self.state.started_mono + payload = { + "event": event, + "session_id": self.state.session_id, + "elapsed_ms": int((time.monotonic() - started) * 1000) if started else 0, + **fields, + } + log = logger.warning if level == "warning" else logger.info + log( + "analysis_event {}", + json.dumps(payload, ensure_ascii=False, separators=(",", ":"), default=str), + ) + def _solve_frame_sync( self, frame: Any, diff --git a/ogscope/main.py b/ogscope/main.py index 8527e9d..d965c75 100644 --- a/ogscope/main.py +++ b/ogscope/main.py @@ -53,6 +53,9 @@ async def main() -> int: port=settings.port, reload=settings.reload, log_level=settings.log_level.lower(), + # Product middleware logs mutations and failures; omit high-rate polling access lines. + # 产品中间件记录写操作与失败;关闭高频轮询的逐请求访问日志。 + access_log=False, ) server = uvicorn.Server(config) await server.serve() diff --git a/ogscope/web/app.py b/ogscope/web/app.py index 70224fe..77650e9 100644 --- a/ogscope/web/app.py +++ b/ogscope/web/app.py @@ -3,6 +3,8 @@ """ import asyncio +import json +import time from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from copy import deepcopy @@ -235,6 +237,26 @@ def _is_local_client(request: Request) -> bool: return client_host in {"127.0.0.1", "::1", "localhost"} +@app.middleware("http") +async def _diagnostic_access_log(request: Request, call_next): + """Log mutations and failed requests without flooding on status polling / 记录写操作与失败,避免状态轮询刷屏。""" + started = time.monotonic() + response = await call_next(request) + if request.method not in {"GET", "HEAD", "OPTIONS"} or response.status_code >= 400: + payload = { + "method": request.method, + "path": request.url.path, + "status": response.status_code, + "duration_ms": int((time.monotonic() - started) * 1000), + } + log = logger.warning if response.status_code >= 400 else logger.info + log( + "http_event {}", + json.dumps(payload, ensure_ascii=False, separators=(",", ":")), + ) + return response + + @app.middleware("http") async def _guard_subordinate_dev_routes(request: Request, call_next): """在 subordinate 角色下限制 dev 接口访问来源 / Limit dev routes in subordinate mode.""" diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index c858eb1..66c14c8 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -233,6 +233,7 @@ async def _fake_stop(): ) assert start.status_code == 200 assert start.json()["state"] == "running" + assert received["session_id"] == start.json()["session_id"] assert received["solve_context"].quality.time_fresh is False assert received["solve_context"].quality.camera_pose_calibrated is False first_session_id = start.json()["session_id"] diff --git a/tests/unit/test_realtime_solve_pipeline.py b/tests/unit/test_realtime_solve_pipeline.py index f340406..c3ae914 100644 --- a/tests/unit/test_realtime_solve_pipeline.py +++ b/tests/unit/test_realtime_solve_pipeline.py @@ -2,7 +2,8 @@ from __future__ import annotations -from unittest.mock import MagicMock +import json +from unittest.mock import MagicMock, patch import numpy as np @@ -42,3 +43,20 @@ def test_completed_realtime_frame_clears_transient_error() -> None: service._apply_solve_result(solved) assert service.state.last_error == "" + + +def test_analysis_event_carries_session_correlation() -> None: + """结构化事件携带会话关联 ID / Structured events carry the session correlation ID.""" + service = RealtimeSolveService() + service.state.session_id = "solve-123" + service.state.started_mono = 1.0 + + with patch("ogscope.core.realtime.service.logger") as mocked_logger: + service._log_event("fullsolve_finished", status="NO_MATCH", detected_stars=2) + + template, payload_raw = mocked_logger.info.call_args.args + payload = json.loads(payload_raw) + assert template == "analysis_event {}" + assert payload["session_id"] == "solve-123" + assert payload["event"] == "fullsolve_finished" + assert payload["status"] == "NO_MATCH" From 3af896f012bbae21359f6c2ce5eaa5fed4dac1d4 Mon Sep 17 00:00:00 2001 From: "Richard.Lu" Date: Tue, 1 Sep 2026 20:53:13 +0800 Subject: [PATCH 23/24] =?UTF-8?q?feat(camera):=20=E8=87=AA=E4=B8=BB?= =?UTF-8?q?=E6=98=9F=E7=A9=BA=E6=9B=9D=E5=85=89=E4=B8=8E=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=85=89=E5=AD=A6=E6=A8=A1=E5=9E=8B=20/=20autonomous=20starfie?= =?UTF-8?q?ld=20AE=20and=20unified=20optics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contracts/core-rest-v1.md | 2 + docs/contracts/core-rest-v1_EN.md | 2 + docs/hardware/bom.md | 3 +- docs/hardware/bom_EN.md | 4 +- ogscope/camera_optics.py | 150 +++++++++++++++++++ ogscope/config.py | 21 ++- ogscope/platform/hardware/camera.py | 135 ++++++++++++++--- ogscope/platform/hardware/tuning/imx327.json | 16 +- ogscope/web/camera_shared.py | 5 +- tests/unit/test_camera_manager_health.py | 8 + tests/unit/test_camera_optics.py | 55 +++++++ tests/unit/test_camera_tuning.py | 128 +++++++++++++++- tests/unit/test_core_contract_api.py | 8 + 13 files changed, 507 insertions(+), 30 deletions(-) create mode 100644 ogscope/camera_optics.py create mode 100644 tests/unit/test_camera_optics.py diff --git a/docs/contracts/core-rest-v1.md b/docs/contracts/core-rest-v1.md index 8c97ac6..bc21a67 100644 --- a/docs/contracts/core-rest-v1.md +++ b/docs/contracts/core-rest-v1.md @@ -76,6 +76,8 @@ - `GET /api/core/v1/camera/status` - 返回相机连接状态、流状态、runtime overrides 与可选 `ambient_hint` - `ambient_hint` 是环境亮度建议遥测,供上层设备做显示/交互策略参考;典型字段包括 `available`、`dark_score`(0.0 明亮到 1.0 昏暗)、`lux`、`exposure_us`、`digital_gain` + - `info.optics` 是可选的产品光学描述;`lens` 保存 16mm F1.4、标称 500 万像素、M12 与红外截止滤镜等名义参数,`full_sensor_fov_deg` 保存 1920×1080 全幅光学视场,`effective_fov_deg` 保存当前采集模式经过产品标定后的有效视场。上层解算与寻星应优先使用 `effective_fov_deg`,字段缺失时再回退本地默认值 + - `info.ae_scene_mode` 与 `info.ae_requested_exposure_mode` 是自主 AE 诊断;`starfield` 表示 OGScope 已独立识别暗天空并选择快门优先的长曝光曲线,不依赖上位机工作模式 - `POST /api/core/v1/camera/start` - 仅当相机启动命令成功且状态确认 `connected=true`、`streaming=true` 时返回 `success=true` - `applied` 包含 `action`、`hardware_plane_ok`、`ready`、`connected`、`streaming`;调用方应以 `ready` 判断是否可立即取帧 diff --git a/docs/contracts/core-rest-v1_EN.md b/docs/contracts/core-rest-v1_EN.md index 676ee66..5923fca 100644 --- a/docs/contracts/core-rest-v1_EN.md +++ b/docs/contracts/core-rest-v1_EN.md @@ -76,6 +76,8 @@ upstream consumers should ignore results from an older session. - `GET /api/core/v1/camera/status` — connection, stream state, runtime overrides, and optional `ambient_hint` - `ambient_hint` is advisory ambient-light telemetry for upstream display/interaction policy. Typical fields: `available`, `dark_score` (0.0 bright to 1.0 dark), `lux`, `exposure_us`, `digital_gain` + - Optional `info.optics` describes product optics. `lens` carries nominal 16mm F1.4, 5MP optical rating, M12, and IR-cut properties; `full_sensor_fov_deg` describes the 1920×1080 optical field, while `effective_fov_deg` is the product-calibrated field for the active capture mode. Upstream solving and sky search should prefer `effective_fov_deg`, with a local fallback for older servers. + - `info.ae_scene_mode` and `info.ae_requested_exposure_mode` diagnose autonomous AE. `starfield` means OGScope independently selected the shutter-first long-exposure curve and does not depend on an upstream work mode. - `POST /api/core/v1/camera/start` - Returns `success=true` only when the start command succeeds and status confirms both `connected=true` and `streaming=true` - `applied` includes `action`, `hardware_plane_ok`, `ready`, `connected`, and `streaming`; callers should use `ready` before requesting frames diff --git a/docs/hardware/bom.md b/docs/hardware/bom.md index f79b0b4..a216739 100644 --- a/docs/hardware/bom.md +++ b/docs/hardware/bom.md @@ -27,8 +27,10 @@ ### IMX327 相机模块 - **传感器**: Sony IMX327 +- **镜头**: 16mm F1.4 低照度镜头,标称 500 万像素,M12 接口,带红外截止滤镜 - **分辨率**: 1920x1080 (2MP) - **像素大小**: 2.9μm x 2.9μm +- **视场**: 1920×1080 全幅名义视场约 19.74° × 11.18°;产品默认 1280×720 模式的标定有效视场约 13.01° × 7.34° - **接口**: MIPI CSI - **灵敏度**: 高感光度,适合极轴镜 - **购买渠道**: 淘宝/AliExpress @@ -109,4 +111,3 @@ --- 最后更新: 2025-01-01 - diff --git a/docs/hardware/bom_EN.md b/docs/hardware/bom_EN.md index 8d51d36..98bcaf9 100644 --- a/docs/hardware/bom_EN.md +++ b/docs/hardware/bom_EN.md @@ -27,8 +27,10 @@ English | [中文](bom.md) ### IMX327 module - **Sensor**: Sony IMX327 +- **Lens**: 16mm F1.4 low-light lens, 5MP optical rating, M12 mount, with IR-cut filter - **Resolution**: 1920×1080 (2MP) -- **Pixel**: 2.9 µm +- **Pixel**: 2.9 µm × 2.9 µm +- **Field of view**: about 19.74° × 11.18° nominal at 1920×1080; about 13.01° × 7.34° product-calibrated for the default 1280×720 mode - **Interface**: MIPI CSI - **Sensitivity**: suited for polar scope use - **Sourcing**: Taobao / AliExpress diff --git a/ogscope/camera_optics.py b/ogscope/camera_optics.py new file mode 100644 index 0000000..970d2f9 --- /dev/null +++ b/ogscope/camera_optics.py @@ -0,0 +1,150 @@ +"""OGScope 产品相机光学模型 / OGScope product camera optics model.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True, slots=True) +class CameraOpticsProfile: + """固定硬件的名义参数与产品标定 / Nominal hardware and product calibration.""" + + profile_id: str + sensor_model: str + sensor_pixel_pitch_um: float + sensor_recording_width_px: int + sensor_recording_height_px: int + lens_focal_length_mm: float + lens_aperture_f_number: float + lens_resolution_rating_mp: float + lens_mount: str + ir_cut_filter: bool + calibrated_focal_length_mm: float + + @staticmethod + def _axis_fov_deg( + pixels: int, pixel_pitch_um: float, focal_length_mm: float + ) -> float: + sensor_size_mm = max(1, int(pixels)) * float(pixel_pitch_um) / 1000.0 + return math.degrees(2.0 * math.atan(sensor_size_mm / (2.0 * focal_length_mm))) + + def _fov_pair( + self, width_px: int, height_px: int, *, calibrated: bool + ) -> tuple[float, float]: + focal_length_mm = ( + self.calibrated_focal_length_mm if calibrated else self.lens_focal_length_mm + ) + return ( + self._axis_fov_deg(width_px, self.sensor_pixel_pitch_um, focal_length_mm), + self._axis_fov_deg(height_px, self.sensor_pixel_pitch_um, focal_length_mm), + ) + + def describe_capture( + self, + *, + capture_width_px: int, + capture_height_px: int, + sampling_mode: str, + rotation_deg: int = 0, + ) -> dict[str, Any]: + """描述当前采集模式的真实有效视场 / Describe effective FOV for the capture mode.""" + mode = str(sampling_mode or "native").lower() + if mode == "supersample": + region_width_px = self.sensor_recording_width_px + region_height_px = self.sensor_recording_height_px + else: + region_width_px = min( + self.sensor_recording_width_px, max(1, int(capture_width_px)) + ) + region_height_px = min( + self.sensor_recording_height_px, max(1, int(capture_height_px)) + ) + + full_width_deg, full_height_deg = self._fov_pair( + self.sensor_recording_width_px, + self.sensor_recording_height_px, + calibrated=False, + ) + theoretical_width_deg, theoretical_height_deg = self._fov_pair( + region_width_px, + region_height_px, + calibrated=False, + ) + effective_width_deg, effective_height_deg = self._fov_pair( + region_width_px, + region_height_px, + calibrated=True, + ) + + rotation = int(rotation_deg) % 360 + if rotation in {90, 270}: + effective_width_deg, effective_height_deg = ( + effective_height_deg, + effective_width_deg, + ) + theoretical_width_deg, theoretical_height_deg = ( + theoretical_height_deg, + theoretical_width_deg, + ) + + return { + "profile_id": self.profile_id, + "sensor": { + "model": self.sensor_model, + "pixel_pitch_um": self.sensor_pixel_pitch_um, + "recording_width_px": self.sensor_recording_width_px, + "recording_height_px": self.sensor_recording_height_px, + }, + "lens": { + "focal_length_mm": self.lens_focal_length_mm, + "aperture_f_number": self.lens_aperture_f_number, + "resolution_rating_mp": self.lens_resolution_rating_mp, + "mount": self.lens_mount, + "ir_cut_filter": self.ir_cut_filter, + }, + "full_sensor_fov_deg": { + "width": round(full_width_deg, 2), + "height": round(full_height_deg, 2), + "source": "nominal_optical_model", + }, + "effective_sensor_region_px": { + "width": region_width_px, + "height": region_height_px, + }, + "theoretical_effective_fov_deg": { + "width": round(theoretical_width_deg, 2), + "height": round(theoretical_height_deg, 2), + }, + "effective_fov_deg": { + "width": round(effective_width_deg, 2), + "height": round(effective_height_deg, 2), + "source": "product_calibrated", + }, + "sampling_mode": mode, + "rotation_deg": rotation, + } + + +# 16 mm 是镜头名义焦距;16.277 mm 来自当前 1280x720 星图解算对 13.01° +# 水平视场的产品标定。两者分开保存,避免把名义规格误当作有效成像视场。 +# 16 mm is the nominal lens focal length; 16.277 mm is the product calibration +# derived from the solved 13.01° horizontal FOV at 1280x720. Keep both values +# so nominal optics are never confused with the effective capture FOV. +IMX327_16MM_F14_OPTICS = CameraOpticsProfile( + profile_id="imx327-16mm-f1.4-ircut-v1", + sensor_model="IMX327", + sensor_pixel_pitch_um=2.9, + sensor_recording_width_px=1920, + sensor_recording_height_px=1080, + lens_focal_length_mm=16.0, + lens_aperture_f_number=1.4, + lens_resolution_rating_mp=5.0, + lens_mount="M12", + ir_cut_filter=True, + calibrated_focal_length_mm=16.277273749463617, +) + +DEFAULT_EFFECTIVE_FOV_WIDTH_DEG = 13.01 +DEFAULT_EFFECTIVE_FOV_HEIGHT_DEG = 7.34 diff --git a/ogscope/config.py b/ogscope/config.py index e00dbad..dca49c8 100644 --- a/ogscope/config.py +++ b/ogscope/config.py @@ -9,6 +9,8 @@ from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +from ogscope.camera_optics import DEFAULT_EFFECTIVE_FOV_WIDTH_DEG + class Settings(BaseSettings): """应用配置 / Application configuration""" @@ -102,13 +104,16 @@ class Settings(BaseSettings): camera_gain: float = Field(default=1.0, description="增益") camera_ae_polar_preset: bool = Field( default=True, - description="自动曝光时启用电子极轴镜 AE 预设 (Shadows/Matrix/Long+EV) / AE polar-scope preset", + description=( + "启用 OGScope 自主星空场景 AE (Shadows/Matrix/Long+EV) / " + "Enable OGScope autonomous starfield AE" + ), ) camera_ae_exposure_value: float = Field( default=1.0, ge=-2.0, le=2.0, - description="AE 曝光补偿(档),与 camera_ae_polar_preset 联用 / AE exposure comp EV stops", + description="星空 AE 曝光补偿(档) / Starfield AE exposure compensation in EV stops", ) camera_ae_aggressive_enabled: bool = Field( default=True, @@ -240,7 +245,11 @@ class Settings(BaseSettings): solver_hint_ra_deg: float = Field(default=0.0, description="默认解算RA提示(度)") solver_hint_dec_deg: float = Field(default=90.0, description="默认解算Dec提示(度)") solver_fov_deg: float = Field( - default=11.0, description="视场角(度) / Default FOV estimate (deg)" + default=DEFAULT_EFFECTIVE_FOV_WIDTH_DEG, + description=( + "当前 IMX327 1280x720 + 16mm 镜头的有效水平视场(度) / " + "Effective horizontal FOV for the current IMX327 1280x720 + 16mm capture" + ), ) solver_max_stars: int = Field(default=80, description="用于解算的最大星点数量") solver_fullsolve_interval_frames: int = Field( @@ -396,12 +405,12 @@ class Settings(BaseSettings): description="相机探测超时(秒)/ Camera probe timeout in seconds", ) camera_capture_timeout_sec: float = Field( - default=4.0, + default=8.0, ge=0.5, le=120.0, description=( - "单次相机抓帧硬超时(秒);应高于允许的最长曝光 / " - "Hard timeout for one camera frame; keep above the longest allowed exposure" + "单次相机抓帧硬超时(秒);1 秒 AE 首帧需要包含多帧收敛预算 / " + "Hard frame timeout; one-second AE startup needs a multi-frame convergence budget" ), ) camera_grab_failures_offline: int = Field( diff --git a/ogscope/platform/hardware/camera.py b/ogscope/platform/hardware/camera.py index 696cf33..ff03a70 100644 --- a/ogscope/platform/hardware/camera.py +++ b/ogscope/platform/hardware/camera.py @@ -14,6 +14,7 @@ import numpy as np +from ogscope.camera_optics import IMX327_16MM_F14_OPTICS from ogscope.domain.camera.driver import CameraCapabilities, LinuxpyV4L2Driver logger = logging.getLogger(__name__) @@ -62,10 +63,12 @@ class IMX327MIPICamera(CameraInterface): """IMX327 MIPI 相机驱动 - 基于 Picamera2 / IMX327 MIPI camera driver - based on Picamera2""" SENSOR_MAX_WIDTH = 1920 - SENSOR_MAX_HEIGHT = 1020 + SENSOR_MAX_HEIGHT = 1080 PREVIEW_BUFFER_COUNT = 2 AE_SUPERVISOR_INTERVAL_S = 1.0 AE_SUPERVISOR_STEP_EV = 0.5 + AE_DARK_CONFIRM_FRAMES = 2 + AE_BRIGHT_CONFIRM_FRAMES = 2 AUTO_EXPOSURE_MAX_US = 1_000_000 LUMINANCE_STATS_MAX_SAMPLES = 32_768 PRODUCT_TUNING_FILE = Path(__file__).with_name("tuning") / "imx327.json" @@ -160,6 +163,14 @@ def __init__(self, config: dict[str, Any]): # 继承夜空增益偏置。 self._ae_effective_exposure_value = 0.0 self._ae_last_adjust_at = 0.0 + self._ae_scene_mode = "evaluating" + self._ae_dark_frames = 0 + self._ae_bright_frames = 0 + self._ae_control_backend = "unavailable" + self._ae_requested_exposure_mode = "long" + self._ae_requested_constraint_mode = "normal" + self._ae_applied_controls: list[str] = [] + self._ae_control_error: str | None = None logger.info( f"初始化 IMX327 MIPI 相机: {self.width}x{self.height}@{self.fps}fps" @@ -308,7 +319,7 @@ def _resolve_sampling_layout( if mode not in {"supersample", "native", "crop"}: mode = "native" if mode == "supersample": - # 先采满幅 1920×1020,再经 _resize_preserve_fov 缩到输出(保留整幅视场,非中心裁切) + # 先采满幅 1920×1080,再经 _resize_preserve_fov 缩到输出(保留整幅视场,非中心裁切) # Full sensor readout then letterbox resize to output (full FOV preserved, not center crop). capture_w = self.SENSOR_MAX_WIDTH capture_h = self.SENSOR_MAX_HEIGHT @@ -625,8 +636,53 @@ def _desired_aggressive_ae_ev(self, stats: dict[str, Any]) -> float: return max(0.5, min(base, 1.0)) return min(base, 0.5) + def _classify_ae_scene(self, stats: dict[str, Any]) -> str: + """用低成本亮度统计识别星空或明亮场景 / Classify starfield or bright scenes cheaply.""" + p50 = float(stats.get("p50") or 0.0) + p99_8 = float(stats.get("p99_8") or stats.get("p99") or 0.0) + saturated = float(stats.get("saturated_fraction") or 0.0) + dark = p50 <= 80.0 and p99_8 <= 200.0 and saturated <= 0.02 + bright = p50 >= 160.0 or saturated >= 0.08 + + self._ae_dark_frames = self._ae_dark_frames + 1 if dark else 0 + self._ae_bright_frames = self._ae_bright_frames + 1 if bright else 0 + if p50 >= 180.0 or saturated >= 0.20: + return "daylight" + if self._ae_bright_frames >= self.AE_BRIGHT_CONFIRM_FRAMES: + return "daylight" + if self._ae_dark_frames >= self.AE_DARK_CONFIRM_FRAMES: + return "starfield" + return self._ae_scene_mode + + @staticmethod + def _load_ae_control_namespace() -> tuple[Any | None, str]: + """优先读取 libcamera 枚举,兼容旧 Picamera2 导出 / Prefer libcamera AE enums.""" + try: + from libcamera import controls as control_namespace + + return control_namespace, "libcamera.controls" + except ImportError: + try: + from picamera2 import controls as control_namespace + + return control_namespace, "picamera2.controls" + except ImportError: + return None, "unavailable" + + def _reset_autonomous_ae_state(self) -> None: + """新 AE 会话从无场景偏见状态开始 / Start a fresh AE session without stale scene bias.""" + self._ae_effective_exposure_value = 0.0 + self._ae_last_adjust_at = 0.0 + self._ae_scene_mode = "evaluating" + self._ae_dark_frames = 0 + self._ae_bright_frames = 0 + self._ae_requested_exposure_mode = "long" + self._ae_requested_constraint_mode = "normal" + self._ae_applied_controls = [] + self._ae_control_error = None + def _update_aggressive_auto_exposure(self, stats: dict[str, Any]) -> None: - """慢速调整 libcamera EV,避免与底层 AE 抢控制 / Slowly supervise libcamera EV.""" + """自主切换星空 AE 并慢速调整 EV / Autonomously select starfield AE and supervise EV.""" if ( not self.camera or not self.auto_exposure @@ -637,6 +693,10 @@ def _update_aggressive_auto_exposure(self, stats: dict[str, Any]) -> None: now = time.monotonic() if now - self._ae_last_adjust_at < self.AE_SUPERVISOR_INTERVAL_S: return + scene_mode = self._classify_ae_scene(stats) + if scene_mode != self._ae_scene_mode: + self._ae_scene_mode = scene_mode + self._apply_polar_auto_exposure_controls() desired = self._desired_aggressive_ae_ev(stats) current = float(self._ae_effective_exposure_value) delta = desired - current @@ -701,7 +761,7 @@ def _camera_capabilities(self) -> dict[str, Any]: } def _apply_polar_auto_exposure_controls(self) -> None: - """libcamera AE 预设:暗部优先、矩阵测光、偏长曝光、EV;失败项跳过 / AE preset; skip unsupported controls.""" + """应用自主场景 AE:星空优先长曝光,明亮场景快速恢复 / Apply autonomous scene AE.""" if not self.camera or not self.auto_exposure: return if not self.ae_polar_preset: @@ -712,19 +772,31 @@ def _apply_polar_auto_exposure_controls(self) -> None: except Exception as e: logger.debug("AeEnable only: %s", e) return - try: - from picamera2 import controls as pcc - except ImportError: - return - cc = getattr(self.camera, "camera_controls", None) or {} updates: dict[str, Any] = {"AeEnable": True} - if hasattr(pcc, "AeConstraintModeEnum"): - updates["AeConstraintMode"] = pcc.AeConstraintModeEnum.Shadows - if hasattr(pcc, "AeMeteringModeEnum"): - updates["AeMeteringMode"] = pcc.AeMeteringModeEnum.Matrix - if hasattr(pcc, "AeExposureModeEnum"): - updates["AeExposureMode"] = pcc.AeExposureModeEnum.Long + pcc, backend = self._load_ae_control_namespace() + self._ae_control_backend = backend + starfield = self._ae_scene_mode == "starfield" + self._ae_requested_constraint_mode = "shadows" if starfield else "normal" + # Long is a shutter-priority curve, not a forced one-second exposure. + # It still shortens exposure in daylight while avoiding noisy gain-first + # behavior in the product's primary starfield use case. + # Long 是快门优先曲线而非强制一秒曝光;白天仍会主动缩短曝光,同时避免 + # 产品核心星空场景退回高增益优先行为。 + self._ae_requested_exposure_mode = "long" + if pcc is not None: + constraint_enum = getattr(pcc, "AeConstraintModeEnum", None) + metering_enum = getattr(pcc, "AeMeteringModeEnum", None) + exposure_enum = getattr(pcc, "AeExposureModeEnum", None) + if constraint_enum is not None and "AeConstraintMode" in cc: + updates["AeConstraintMode"] = getattr( + constraint_enum, + "Shadows" if starfield else "Normal", + ) + if metering_enum is not None and "AeMeteringMode" in cc: + updates["AeMeteringMode"] = metering_enum.Matrix + if exposure_enum is not None and "AeExposureMode" in cc: + updates["AeExposureMode"] = exposure_enum.Long ev = float(self._ae_effective_exposure_value) # Explicitly write the neutral value too. Omitting zero can leave a # previous dark-scene bias active after returning to daylight. @@ -738,19 +810,33 @@ def _apply_polar_auto_exposure_controls(self) -> None: self._apply_frame_duration_controls() self._apply_ae_flicker_controls() self.camera.set_controls(updates) + self._ae_applied_controls = sorted(updates) + self._ae_control_error = None logger.info( - "已应用电子极轴镜 AE 预设 (Shadows/Matrix/Long, EV≈%.2f)", + "已应用自主 AE 场景=%s constraint=%s exposure=%s EV≈%.2f / " + "Applied autonomous AE scene=%s constraint=%s exposure=%s EV≈%.2f", + self._ae_scene_mode, + self._ae_requested_constraint_mode, + self._ae_requested_exposure_mode, + ev, + self._ae_scene_mode, + self._ae_requested_constraint_mode, + self._ae_requested_exposure_mode, ev, ) except Exception as e: + self._ae_control_error = type(e).__name__ logger.warning("AE 预设批量设置失败,逐项重试: %s", e) + applied: list[str] = [] for key, val in updates.items(): if key != "AeEnable" and key not in cc: continue try: self.camera.set_controls({key: val}) + applied.append(key) except Exception as err: logger.debug("AE 控制 %s 未生效: %s", key, err) + self._ae_applied_controls = sorted(applied) def _white_balance_controls(self) -> dict[str, Any]: """生成白平衡控制;auto 必须真正打开 AWB / Build WB controls; auto must really enable AWB.""" @@ -936,6 +1022,7 @@ def start_capture(self) -> bool: self.camera.start() self.is_capturing = True if self.auto_exposure: + self._reset_autonomous_ae_state() self._apply_polar_auto_exposure_controls() logger.info("相机开始捕获") return True @@ -1234,8 +1321,7 @@ def set_auto_exposure(self, enabled: bool) -> bool: try: self.auto_exposure = enabled if enabled: - self._ae_effective_exposure_value = 0.0 - self._ae_last_adjust_at = 0.0 + self._reset_autonomous_ae_state() self._apply_polar_auto_exposure_controls() else: self.camera.set_controls({"AeEnable": False}) @@ -1364,11 +1450,18 @@ def get_camera_info(self) -> dict[str, Any]: camera_properties = self.camera.camera_properties metadata = self._last_metadata or {} capabilities = self._camera_capabilities() + optics = IMX327_16MM_F14_OPTICS.describe_capture( + capture_width_px=self.capture_width, + capture_height_px=self.capture_height, + sampling_mode=self.sampling_mode, + rotation_deg=self.rotation, + ) return { "driver": self.driver_name, "backend": self.backend_name, "capabilities": capabilities, "sensor": camera_properties.get("Model", "Unknown"), + "optics": optics, "resolution": f"{self.width}x{self.height}", "fps": self.fps, "exposure_us": self.exposure_us, @@ -1424,6 +1517,12 @@ def get_camera_info(self) -> dict[str, Any]: "ae_exposure_value": self.ae_exposure_value, "ae_effective_exposure_value": self._ae_effective_exposure_value, "ae_aggressive_enabled": self.ae_aggressive_enabled, + "ae_scene_mode": self._ae_scene_mode, + "ae_control_backend": self._ae_control_backend, + "ae_requested_exposure_mode": self._ae_requested_exposure_mode, + "ae_requested_constraint_mode": self._ae_requested_constraint_mode, + "ae_applied_controls": self._ae_applied_controls, + "ae_control_error": self._ae_control_error, "lores_enabled": self.lores_enabled, "lores_available": self._lores_available, "lores_width": self.lores_width, diff --git a/ogscope/platform/hardware/tuning/imx327.json b/ogscope/platform/hardware/tuning/imx327.json index 480e36a..fa8068b 100644 --- a/ogscope/platform/hardware/tuning/imx327.json +++ b/ogscope/platform/hardware/tuning/imx327.json @@ -1,7 +1,7 @@ { "version": 2.0, "target": "bcm2835", - "description": "OGScope 产品自维护 IMX327 tuning,长曝光曲线扩展至 1 秒 / OGScope product-owned IMX327 tuning with a one-second long-exposure curve.", + "description": "OGScope 产品自维护 IMX327 + 16mm F1.4 tuning,星空曲线优先快门并扩展至 1 秒 / OGScope product-owned IMX327 + 16mm F1.4 tuning with shutter-first starfield exposure up to one second.", "algorithms": [ { "rpi.black_level": @@ -88,7 +88,7 @@ "long": { "shutter": [ 1000, 30000, 60000, 120000, 250000, 500000, 1000000, 1000000, 1000000, 1000000 ], - "gain": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 4.0, 12.0 ] + "gain": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 4.0, 4.0 ] } }, "constraint_modes": @@ -115,6 +115,18 @@ 1000, 0.8 ] } + ], + "shadows": [ + { + "bound": "LOWER", + "q_lo": 0.0, + "q_hi": 0.5, + "y_target": + [ + 0, 0.17, + 1000, 0.17 + ] + } ] }, "y_target": diff --git a/ogscope/web/camera_shared.py b/ogscope/web/camera_shared.py index 8ecabe5..409ed88 100644 --- a/ogscope/web/camera_shared.py +++ b/ogscope/web/camera_shared.py @@ -65,7 +65,10 @@ def __init__(self) -> None: self._probe_timeout_sec = max(0.5, float(settings.camera_probe_timeout_sec)) self._capture_timeout_sec = max(0.5, float(settings.camera_capture_timeout_sec)) self._grabber_drain_timeout_sec = self._capture_timeout_sec + 1.0 - self._stop_timeout_sec = 4.0 + # 停止预算必须覆盖仍在收敛的最长抓帧,否则一次正常的 1 秒 AE 首帧超时 + # 会把 libcamera 留在不可回收状态。 / Stop must outlive an in-flight + # long-AE convergence frame or a recoverable startup becomes unrecoverable. + self._stop_timeout_sec = max(4.0, self._capture_timeout_sec + 2.0) self._close_timeout_sec = 2.5 self._stale_timeout_sec = max( 0.5, float(settings.camera_frame_stale_timeout_sec) diff --git a/tests/unit/test_camera_manager_health.py b/tests/unit/test_camera_manager_health.py index 098e5ae..1e3b372 100644 --- a/tests/unit/test_camera_manager_health.py +++ b/tests/unit/test_camera_manager_health.py @@ -171,6 +171,14 @@ def test_preview_fps_is_independent_runtime_setting() -> None: assert manager._target_fps == 12 +def test_lifecycle_stop_budget_outlives_long_exposure_capture() -> None: + """停止预算必须覆盖长曝光抓帧收敛 / Stop budget must outlive long-AE capture.""" + manager = CameraManager() + + assert manager._capture_timeout_sec >= 8.0 + assert manager._stop_timeout_sec >= manager._capture_timeout_sec + 2.0 + + @pytest.mark.asyncio async def test_stop_timeout_keeps_camera_handle_and_blocks_reacquire() -> None: """stop 超时后不得丢弃仍占用 libcamera 的实例 / Keep the handle after stop timeout.""" diff --git a/tests/unit/test_camera_optics.py b/tests/unit/test_camera_optics.py new file mode 100644 index 0000000..0386eb2 --- /dev/null +++ b/tests/unit/test_camera_optics.py @@ -0,0 +1,55 @@ +"""产品光学模型测试 / Product optics model tests.""" + +from __future__ import annotations + +import pytest + +from ogscope.camera_optics import IMX327_16MM_F14_OPTICS + + +@pytest.mark.unit +def test_imx327_16mm_profile_separates_full_and_effective_fov() -> None: + """全幅名义视场不能冒充 720p 有效视场 / Full nominal FOV cannot replace 720p effective FOV.""" + optics = IMX327_16MM_F14_OPTICS.describe_capture( + capture_width_px=1280, + capture_height_px=720, + sampling_mode="native", + rotation_deg=180, + ) + + assert optics["sensor"] == { + "model": "IMX327", + "pixel_pitch_um": 2.9, + "recording_width_px": 1920, + "recording_height_px": 1080, + } + assert optics["lens"] == { + "focal_length_mm": 16.0, + "aperture_f_number": 1.4, + "resolution_rating_mp": 5.0, + "mount": "M12", + "ir_cut_filter": True, + } + assert optics["full_sensor_fov_deg"]["width"] == pytest.approx(19.74) + assert optics["full_sensor_fov_deg"]["height"] == pytest.approx(11.18) + assert optics["theoretical_effective_fov_deg"]["width"] == pytest.approx(13.23) + assert optics["effective_fov_deg"] == { + "width": 13.01, + "height": 7.34, + "source": "product_calibrated", + } + + +@pytest.mark.unit +def test_supersample_uses_full_sensor_region_and_rotation_swaps_axes() -> None: + """全幅超采样保留视场且旋转交换输出轴 / Supersampling preserves FOV; rotation swaps axes.""" + optics = IMX327_16MM_F14_OPTICS.describe_capture( + capture_width_px=1280, + capture_height_px=720, + sampling_mode="supersample", + rotation_deg=90, + ) + + assert optics["effective_sensor_region_px"] == {"width": 1920, "height": 1080} + assert optics["effective_fov_deg"]["width"] == pytest.approx(11.0, abs=0.02) + assert optics["effective_fov_deg"]["height"] == pytest.approx(19.42, abs=0.02) diff --git a/tests/unit/test_camera_tuning.py b/tests/unit/test_camera_tuning.py index f7b0807..fc55b99 100644 --- a/tests/unit/test_camera_tuning.py +++ b/tests/unit/test_camera_tuning.py @@ -35,7 +35,133 @@ def test_product_tuning_long_curve_reaches_one_second() -> None: assert len(long_mode["shutter"]) == len(long_mode["gain"]) first_max_shutter = long_mode["shutter"].index(1_000_000) assert long_mode["gain"][: first_max_shutter + 1] == [1.0] * (first_max_shutter + 1) - assert long_mode["gain"][-1] == 12.0 + assert max(long_mode["gain"]) == 4.0 + assert agc["constraint_modes"]["shadows"][0]["q_hi"] == pytest.approx(0.5) + + +@pytest.mark.unit +def test_autonomous_ae_selects_libcamera_long_shadows_for_starfield( + monkeypatch, +) -> None: + """暗场确认后必须请求 Long+Shadows / Confirmed darkness must request Long+Shadows.""" + + class Constraint: + Normal = "constraint-normal" + Shadows = "constraint-shadows" + + class Metering: + Matrix = "metering-matrix" + + class Exposure: + Normal = "exposure-normal" + Long = "exposure-long" + + class Namespace: + AeConstraintModeEnum = Constraint + AeMeteringModeEnum = Metering + AeExposureModeEnum = Exposure + + class FakeCamera: + camera_controls = { + "AeConstraintMode": object(), + "AeMeteringMode": object(), + "AeExposureMode": object(), + "ExposureValue": object(), + "FrameDurationLimits": object(), + } + + def __init__(self) -> None: + self.controls: list[dict[str, Any]] = [] + + def set_controls(self, controls: dict[str, Any]) -> None: + self.controls.append(controls) + + camera = _camera(ae_exposure_value=1.0) + fake = FakeCamera() + camera.camera = fake + monkeypatch.setattr( + camera, + "_load_ae_control_namespace", + lambda: (Namespace, "test.libcamera.controls"), + ) + dark = { + "p50": 8.0, + "p90": 10.0, + "p99": 12.0, + "p99_8": 13.0, + "saturated_fraction": 0.0, + } + + camera._update_aggressive_auto_exposure(dark) + camera._ae_last_adjust_at = 0.0 + camera._update_aggressive_auto_exposure(dark) + + scene_update = next(item for item in fake.controls if "AeExposureMode" in item) + assert camera._ae_scene_mode == "starfield" + assert scene_update["AeExposureMode"] == "exposure-long" + assert scene_update["AeConstraintMode"] == "constraint-shadows" + assert camera._ae_control_backend == "test.libcamera.controls" + + +@pytest.mark.unit +def test_autonomous_ae_immediately_escapes_severe_brightness(monkeypatch) -> None: + """严重过曝必须立即退回日间曲线 / Severe clipping must immediately restore daylight AE.""" + + class Constraint: + Normal = "constraint-normal" + Shadows = "constraint-shadows" + + class Metering: + Matrix = "metering-matrix" + + class Exposure: + Normal = "exposure-normal" + Long = "exposure-long" + + class Namespace: + AeConstraintModeEnum = Constraint + AeMeteringModeEnum = Metering + AeExposureModeEnum = Exposure + + class FakeCamera: + camera_controls = { + "AeConstraintMode": object(), + "AeMeteringMode": object(), + "AeExposureMode": object(), + "ExposureValue": object(), + "FrameDurationLimits": object(), + } + + def __init__(self) -> None: + self.controls: list[dict[str, Any]] = [] + + def set_controls(self, controls: dict[str, Any]) -> None: + self.controls.append(controls) + + camera = _camera(ae_exposure_value=1.0) + fake = FakeCamera() + camera.camera = fake + camera._ae_scene_mode = "starfield" + camera._ae_effective_exposure_value = 1.5 + monkeypatch.setattr( + camera, "_load_ae_control_namespace", lambda: (Namespace, "test") + ) + + camera._update_aggressive_auto_exposure( + { + "p50": 220.0, + "p90": 250.0, + "p99": 255.0, + "p99_8": 255.0, + "saturated_fraction": 0.25, + } + ) + + scene_update = next(item for item in fake.controls if "AeExposureMode" in item) + assert camera._ae_scene_mode == "daylight" + assert scene_update["AeExposureMode"] == "exposure-long" + assert scene_update["AeConstraintMode"] == "constraint-normal" + assert camera._ae_effective_exposure_value == pytest.approx(0.0) @pytest.mark.unit diff --git a/tests/unit/test_core_contract_api.py b/tests/unit/test_core_contract_api.py index c858eb1..a1227a7 100644 --- a/tests/unit/test_core_contract_api.py +++ b/tests/unit/test_core_contract_api.py @@ -71,6 +71,13 @@ def test_core_camera_ambient_hint_from_metadata() -> None: "actual_exposure_us": 80_000, "auto_exposure_max_us": 100_000, "actual_digital_gain": 2.0, + "optics": { + "effective_fov_deg": { + "width": 13.01, + "height": 7.34, + "source": "product_calibrated", + } + }, }, } ) @@ -78,6 +85,7 @@ def test_core_camera_ambient_hint_from_metadata() -> None: hint = normalized["ambient_hint"] assert hint["available"] is True assert hint["source"] == "camera_metadata" + assert normalized["info"]["optics"]["effective_fov_deg"]["width"] == 13.01 assert 0.0 <= hint["dark_score"] <= 1.0 assert hint["exposure_us"] == 80_000 From 1723ea34dafdb439dcadee72b183097148c778fb Mon Sep 17 00:00:00 2001 From: "Richard.Lu" Date: Tue, 1 Sep 2026 22:17:59 +0800 Subject: [PATCH 24/24] =?UTF-8?q?style(camera):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=B5=81=E5=AA=92=E4=BD=93=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=20/=20normalize=20camera=20streaming=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ogscope/domain/camera/services.py | 4 +--- ogscope/domain/camera/stream_limiter.py | 5 ++++- ogscope/domain/camera/streaming.py | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ogscope/domain/camera/services.py b/ogscope/domain/camera/services.py index 1010cf3..984709e 100644 --- a/ogscope/domain/camera/services.py +++ b/ogscope/domain/camera/services.py @@ -164,9 +164,7 @@ async def get_stream_status(self) -> dict[str, Any]: metrics = await get_camera_manager().stream_metrics() limiter_metrics = await limiter.snapshot() - configured_stall_timeout_ms = int( - settings.stream_mjpeg_client_stall_timeout_ms - ) + configured_stall_timeout_ms = int(settings.stream_mjpeg_client_stall_timeout_ms) effective_stall_timeout_ms = ( max( configured_stall_timeout_ms, diff --git a/ogscope/domain/camera/stream_limiter.py b/ogscope/domain/camera/stream_limiter.py index 4a87109..d3e33f0 100644 --- a/ogscope/domain/camera/stream_limiter.py +++ b/ogscope/domain/camera/stream_limiter.py @@ -81,7 +81,10 @@ async def snapshot(self) -> dict[str, object]: * 1000 ), "oldest_client_idle_ms": int( - max((now - state.last_progress_mono for state in states), default=0.0) + max( + (now - state.last_progress_mono for state in states), + default=0.0, + ) * 1000 ), "released_clients_total": sum(self._release_reasons.values()), diff --git a/ogscope/domain/camera/streaming.py b/ogscope/domain/camera/streaming.py index f424e09..f928d8f 100644 --- a/ogscope/domain/camera/streaming.py +++ b/ogscope/domain/camera/streaming.py @@ -24,7 +24,6 @@ from ogscope.web.camera_shared import get_camera_manager from ogscope.web.mjpeg_stream_helpers import mjpeg_sleep_or_disconnect - _cleanup_tasks: set[asyncio.Task[Any]] = set() _module_logger = logging.getLogger(__name__)