From 78e1308063b44c250c9da8758f0dbdae3ea8d17c Mon Sep 17 00:00:00 2001 From: eric Date: Tue, 19 May 2026 06:47:07 +0000 Subject: [PATCH] ss --- d2ypp2/.gitignore | 4 + d2ypp2/README.md | 26 +- d2ypp2/config/system-stack.env.example | 2 + d2ypp2/docs/ARCHITECTURE.md | 20 +- d2ypp2/docs/HOMELAB_OPTIMIZATION_PLAN.md | 211 +++++++ d2ypp2/install.sh | 428 +++++++++++++- d2ypp2/scripts/launch.py | 3 + d2ypp2/scripts/linux/install_all.sh | 0 d2ypp2/scripts/linux/install_business.sh | 0 d2ypp2/scripts/linux/install_hub.sh | 0 d2ypp2/scripts/linux/install_stack.sh | 0 d2ypp2/scripts/linux/launch_browser.sh | 0 d2ypp2/scripts/linux/lib/common.sh | 157 ++++- d2ypp2/scripts/linux/live_edge_watch.sh | 53 ++ d2ypp2/scripts/linux/neko_ip_watch.sh | 8 +- d2ypp2/scripts/linux/probe_live_access.sh | 0 .../linux/refresh_live_console_unit.sh | 0 d2ypp2/scripts/linux/reinstall_live_edge.sh | 0 d2ypp2/scripts/linux/run_kmscube.sh | 0 d2ypp2/scripts/linux/service_ctl.sh | 207 ++++--- d2ypp2/scripts/linux/uninstall_stack.sh | 3 +- d2ypp2/scripts/linux/upgrade_ffmpeg_newer.sh | 0 d2ypp2/scripts/linux/upgrade_live_console.sh | 0 d2ypp2/scripts/linux/webtty_shell.sh | 0 d2ypp2/scripts/srt_to_hdmi_drm.example.sh | 0 d2ypp2/services/neko/browser-launch.sh | 1 - d2ypp2/services/neko/chromium.conf | 10 + d2ypp2/services/neko/google-chrome.conf | 10 + d2ypp2/services/neko/policies/policies.json | 7 +- .../tampermonkey-provisioning.json | 5 +- d2ypp2/src/android_control.py | 23 +- d2ypp2/src/control_plane.py | 67 ++- d2ypp2/src/hub_routes.py | 6 +- d2ypp2/src/neko_capacity.py | 112 ++++ d2ypp2/tests/test_android_redroid.py | 13 +- d2ypp2/tests/test_control_plane_services.py | 25 +- d2ypp2/tests/test_install_bootstrap.py | 134 +++++ d2ypp2/tests/test_live_edge_watchdog.py | 37 ++ d2ypp2/tests/test_neko_capacity.py | 96 ++++ d2ypp2/tests/test_neko_config.py | 71 ++- d2ypp2/tests/test_verify_local_install.py | 188 ++++++ d2ypp2/tools/render_neko_compose.py | 122 ++++ .../tools/render_tampermonkey_provisioning.py | 5 +- d2ypp2/tools/verify_local_install.py | 535 ++++++++++++++++++ .../live-product/LiveAndroidStreamConsole.tsx | 52 +- .../live-product/LiveControlApp.tsx | 38 +- d2ypp2/web-console/lib/scrcpyH264Decoder.ts | 2 + d2ypp2/web-console/out/.build-stamp.json | 4 +- d2ypp2/web-console/out/404.html | 2 +- .../chunks/app/page-10361036b8bf1ca2.js | 1 + .../chunks/app/page-2e4163a91e299843.js | 1 - ...47d040ae16366.css => 693f7950f3222bce.css} | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 d2ypp2/web-console/out/android.html | 2 +- d2ypp2/web-console/out/android.txt | 2 +- d2ypp2/web-console/out/console.html | 2 +- d2ypp2/web-console/out/console.txt | 2 +- d2ypp2/web-console/out/index.html | 2 +- d2ypp2/web-console/out/index.txt | 4 +- d2ypp2/web-console/out/shellcrash.html | 2 +- d2ypp2/web-console/out/shellcrash.txt | 2 +- d2ypp2/web.py | 4 +- 63 files changed, 2527 insertions(+), 186 deletions(-) create mode 100644 d2ypp2/docs/HOMELAB_OPTIMIZATION_PLAN.md mode change 100644 => 100755 d2ypp2/scripts/linux/install_all.sh mode change 100644 => 100755 d2ypp2/scripts/linux/install_business.sh mode change 100644 => 100755 d2ypp2/scripts/linux/install_hub.sh mode change 100644 => 100755 d2ypp2/scripts/linux/install_stack.sh mode change 100644 => 100755 d2ypp2/scripts/linux/launch_browser.sh mode change 100644 => 100755 d2ypp2/scripts/linux/lib/common.sh create mode 100755 d2ypp2/scripts/linux/live_edge_watch.sh mode change 100644 => 100755 d2ypp2/scripts/linux/neko_ip_watch.sh mode change 100644 => 100755 d2ypp2/scripts/linux/probe_live_access.sh mode change 100644 => 100755 d2ypp2/scripts/linux/refresh_live_console_unit.sh mode change 100644 => 100755 d2ypp2/scripts/linux/reinstall_live_edge.sh mode change 100644 => 100755 d2ypp2/scripts/linux/run_kmscube.sh mode change 100644 => 100755 d2ypp2/scripts/linux/service_ctl.sh mode change 100644 => 100755 d2ypp2/scripts/linux/uninstall_stack.sh mode change 100644 => 100755 d2ypp2/scripts/linux/upgrade_ffmpeg_newer.sh mode change 100644 => 100755 d2ypp2/scripts/linux/upgrade_live_console.sh mode change 100644 => 100755 d2ypp2/scripts/linux/webtty_shell.sh mode change 100644 => 100755 d2ypp2/scripts/srt_to_hdmi_drm.example.sh mode change 100644 => 100755 d2ypp2/services/neko/browser-launch.sh create mode 100644 d2ypp2/src/neko_capacity.py create mode 100644 d2ypp2/tests/test_install_bootstrap.py create mode 100644 d2ypp2/tests/test_live_edge_watchdog.py create mode 100644 d2ypp2/tests/test_neko_capacity.py create mode 100644 d2ypp2/tests/test_verify_local_install.py create mode 100644 d2ypp2/tools/render_neko_compose.py create mode 100755 d2ypp2/tools/verify_local_install.py create mode 100644 d2ypp2/web-console/out/_next/static/chunks/app/page-10361036b8bf1ca2.js delete mode 100644 d2ypp2/web-console/out/_next/static/chunks/app/page-2e4163a91e299843.js rename d2ypp2/web-console/out/_next/static/css/{f9347d040ae16366.css => 693f7950f3222bce.css} (55%) rename d2ypp2/web-console/out/_next/static/{KvIE-N5Gjg_4vh4xEAPuU => z_UuUy3mGDwiCWfhwZJVM}/_buildManifest.js (100%) rename d2ypp2/web-console/out/_next/static/{KvIE-N5Gjg_4vh4xEAPuU => z_UuUy3mGDwiCWfhwZJVM}/_ssgManifest.js (100%) diff --git a/d2ypp2/.gitignore b/d2ypp2/.gitignore index 920b054..b0a9433 100644 --- a/d2ypp2/.gitignore +++ b/d2ypp2/.gitignore @@ -182,6 +182,10 @@ vendor/scrcpy-server.jar # 无 PM2 时的本地进程状态 .process_runner/ +# 本地排障/运行时生成物 +tmp/ +services/neko/docker-compose.generated.yml + # 本机端口/主机覆盖(从 config/runtime.env.example 复制) config/runtime.env config/system-stack.env diff --git a/d2ypp2/README.md b/d2ypp2/README.md index 9fd79ea..750a67e 100644 --- a/d2ypp2/README.md +++ b/d2ypp2/README.md @@ -8,7 +8,7 @@ - 抖音直播间 → YouTube Live:支持单频道 / 多频道同时推流,每个任务独立 `youtube.ini`、`URL_config` 与 PM2 进程 - TikTok 直播:支持视频流转发到硬件 HDMI,也可配合 SRS / OBS 接流后再转发 -- Neko 浏览器:默认保留管理员会话、开发者工具与 Tampermonkey 持久化 +- Neko 浏览器:默认保留管理员会话、开发者工具与 Tampermonkey 持久化;按 `NEKO_INSTANCE_COUNT` 动态生成独立容器,默认保底 2 个,YouTube PRO 频道保存会自动提升容量 - 兼容架构:Linux `x86_64` / `arm64`,使用一键脚本部署,控制面板默认域名 `live.local` ## Linux Unmanned Live Profiles @@ -18,7 +18,7 @@ This fork now includes a modular Linux deployment layout for ARM and X86 nodes. ### Live Platform(四层架构) - **配置中心**:`/opt/live/config`(模板见 `live-platform/config/defaults/`,开发时回退到仓库内同路径) -- **一键脚本**:`./install-core.sh`(系统 + 基础服务,与业务解耦)、`./install-live.sh`(仅无人直播业务)、`./doctor.sh`(体检) +- **一键脚本**:`install.sh` 是远程 bootstrap 入口,负责源码获取、日志、锁、安装分发与验收;`./install-core.sh`(系统 + 基础服务,与业务解耦)、`./install-live.sh`(仅无人直播业务)、`./doctor.sh`(体检) - **统一 API**:`GET /hub/dashboard`、`GET /hub/config` 等(见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)) - **产品级 Web**:根路径 `/` 为无人直播系统控制台;`/console` 为高级全功能页 @@ -26,9 +26,28 @@ This fork now includes a modular Linux deployment layout for ARM and X86 nodes. - `install-business.sh`: Douyin / YouTube / HDMI business module with ffmpeg, mpv, ADB, Chromium, web-scrcpy and SRS - `install-all.sh`: installs both layers without tightly coupling them +### One-Line Install + +Fresh Debian / Ubuntu / Armbian style hosts use the stream-safe bootstrapper: + +```bash +curl -fsSL https:///install.sh | bash +``` + +The checked-in `install.sh` is the production contract for that URL: it writes logs to `/var/log/shd2ypp2`, uses `/run/lock/shd2ypp2-install.lock` to prevent concurrent installs, syncs the project to `/opt/live/d2ypp`, dispatches `scripts/linux/install_stack.sh all`, waits for `/health`, runs `/deploy/check`, and then runs `tools/verify_local_install.py`. + +The current GitLab remote at `gitlab.nomadro.com` requires authentication for raw file and clone access from a fresh machine. Publish the same `install.sh` behind an unauthenticated HTTPS endpoint, or pass a credentialed/public source explicitly: + +```bash +LIVE_REPO_URL=http://gitlab.nomadro.com/gitlab-instance-0a899031/sh.git \ +LIVE_REPO_REF=best \ +curl -fsSL https:///install.sh | bash -s -- --profile hub +``` + Detailed docs: - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) +- [`docs/HOMELAB_OPTIMIZATION_PLAN.md`](docs/HOMELAB_OPTIMIZATION_PLAN.md) - [`docs/install-profiles.md`](docs/install-profiles.md) - [`docs/super-hub-architecture.md`](docs/super-hub-architecture.md) @@ -36,7 +55,7 @@ Detailed docs: After `install-hub.sh` or `install-all.sh`, the default service map is: -- **`http://live.local`** (port 80, via **Caddy** `live-edge.service`) → same UI/API as the control plane +- **`http://live.local`** (port 80, via **Caddy** `live-edge.service` + `live-edge-watch.service`) → same UI/API as the control plane - `http://live.local:8001` for direct access to FastAPI (no edge proxy) - `http://live.local/shellcrash` or `:8001/shellcrash` — ShellCrash 编辑器 - `http://live.local/android` or `:8001/android` — 安卓中心 @@ -51,6 +70,7 @@ After deployment, validate the real delivery path instead of only checking one p - `sudo bash doctor.sh` - `sudo bash probe-live.sh` +- `python3 tools/verify_local_install.py --install-dir /opt/live/d2ypp` - `python3 tools/verify_live_console.py` with `LIVE_DEPLOY_HOST / LIVE_DEPLOY_USER / LIVE_DEPLOY_PASS` when verifying a remote node The control plane exposes `GET /deploy/check` for a structured report covering `live.local`, `:80`, `:8001`, LAN IP binding, exported web assets, and `system-stack.env`. diff --git a/d2ypp2/config/system-stack.env.example b/d2ypp2/config/system-stack.env.example index 2d54325..0239231 100644 --- a/d2ypp2/config/system-stack.env.example +++ b/d2ypp2/config/system-stack.env.example @@ -12,6 +12,7 @@ PORT=8001 # Caddy 将 :80 反代到 PORT(http://live.local 无端口验收) ENABLE_LIVE_EDGE_PROXY=1 +LIVE_EDGE_WATCH_INTERVAL=15 # Hub web services WEBTTY_PORT=7681 @@ -67,6 +68,7 @@ NEKO_START_URL=https://accounts.google.com/ServiceLogin?continue=https://www.you NEKO_IMAGE_X86=ghcr.io/m1k1o/neko/google-chrome:3.0.0 NEKO_IMAGE_ARM64=ghcr.io/m1k1o/neko/chromium:3.0.0 NEKO_INSTANCE_COUNT=2 +NEKO_MAX_INSTANCE_COUNT=64 # Neko 浏览器登录:显示昵称可填 live / admin;密码与全站策略一致(默认 12345678) NEKO_USER_PASS=12345678 NEKO_ADMIN_PASS=12345678 diff --git a/d2ypp2/docs/ARCHITECTURE.md b/d2ypp2/docs/ARCHITECTURE.md index ac30100..052c63b 100644 --- a/d2ypp2/docs/ARCHITECTURE.md +++ b/d2ypp2/docs/ARCHITECTURE.md @@ -1,5 +1,7 @@ # Live Platform 四层架构 +Homelab 一键直播系统的具体优化路线图见 [`HOMELAB_OPTIMIZATION_PLAN.md`](HOMELAB_OPTIMIZATION_PLAN.md),该文档把 `live.local` 面板、YouTube 多频道 Pro、Neko、TikTok HDMI、安卓云控和网络代理拆成可验收的实施项。 + ## 分层 | 层 | 职责 | 代码/路径 | @@ -39,7 +41,23 @@ ## live.local(无端口验收) -- **`install_hub.sh` 末尾**执行 **`install_live_edge_proxy`**:安装/启用 **`live-edge.service`(Caddy)**,将 **`http://live.local`(:80)** 反代到 **`127.0.0.1:${PORT}`**(默认 8001)。 +- **`install_hub.sh` 末尾**执行 **`install_live_edge_proxy`**:安装/启用 **`live-edge.service`(Caddy)**,将 **`http://live.local`(:80)** 反代到 **`127.0.0.1:${PORT}`**(默认 8001),并启用 **`live-edge-watch.service`** 自动拉起/修复 80 入口。 - **Homepage** 默认改绑 **`HOMEPAGE_PORT=3080`**,避免与 Caddy 抢 80。 - 仅更新反代:`sudo bash scripts/linux/reinstall_live_edge.sh` - `ENABLE_LIVE_EDGE_PROXY=0` 可关闭(`system-stack.env`)。 + +## Neko 动态实例 + +- `scripts/linux/service_ctl.sh neko start|restart` 与安装流程都会先调用 `tools/render_neko_compose.py`,输出 `services/neko/docker-compose.generated.yml`,再由 Docker Compose 启动 `live-neko-1..N`。 +- `NEKO_INSTANCE_COUNT` 最少为 2,`NEKO_MAX_INSTANCE_COUNT` 默认封顶 64;YouTube PRO 保存频道时由 `src/neko_capacity.py` 把实例数提升到频道数所需容量。 +- 每个实例独立分配 profile、HTTP 端口、TCPMUX、UDPMUX 和 UDP range。第 4 个及以后实例从 `52400-52499` 起按百段分配 UDP range,避免与前 3 个实例的 mux 端口冲突。 +- 新 profile 启动前会调用 `seed_neko_tampermonkey_profile`,从 1 号健康 profile 补种 Tampermonkey 扩展目录、脚本存储和 managed settings;这样新频道新 Neko 不依赖 Chrome Web Store 当场下载,弱网/中国网络下也能看到默认脚本。 +- `services/neko/browser-launch.sh` 必须保持可执行位;本地 verifier 会检查每个 Neko 里的 `google-chrome`/`chromium` supervisor 进程,防止只看到 Neko 容器健康但浏览器实际没启动。 +- `services/neko/docker-compose.yml` 仅保留为兼容模板和排障参考;生产运行以生成文件为准。 +- `live-neko-ip-watch.service` 监测 LAN IP 变化,更新 `NEKO_WEBRTC_NAT1TO1` 后重启 Neko,避免换网后 WebRTC、iframe 或局域网入口继续指向旧地址。 + +## 安卓 Web 控制 + +- `/android/screenshot` 提供 PNG 截图兜底,`/android/action` 支持 tap、double tap、long press、text、keyevent、swipe 等操作。 +- 前端优先尝试 `/android/scrcpy/ws` 真流;WebSocket、WebCodecs 或 scrcpy-server 不可用时自动降级到截图轮询,仍可按截图坐标触控。 +- Redroid 和真实 ADB 设备统一走 ADB 发现;Redroid 启动后自动尝试 `adb connect 127.0.0.1:5555`。 diff --git a/d2ypp2/docs/HOMELAB_OPTIMIZATION_PLAN.md b/d2ypp2/docs/HOMELAB_OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..ede8fff --- /dev/null +++ b/d2ypp2/docs/HOMELAB_OPTIMIZATION_PLAN.md @@ -0,0 +1,211 @@ +# Homelab 一键直播系统优化方案 + +本方案把 `/home/eric/sh/d2ypp2` 作为核心源码与发布来源,目标是在 x86_64 / arm64 Linux 主机上通过一键脚本安装统一服务,并在 `http://live.local` 提供可展示、可控制、可诊断的 Web 面板。 + +## 目标状态 + +- 一键安装:`install-all.sh` 可在 Debian / Ubuntu / Armbian 类系统上完成 Core OS、Infra Service、Business、Web Console 四层安装;`install-core.sh` 与 `install-live.sh` 可独立运行。 +- 统一入口:`live.local` 由 Caddy `live-edge.service` 承接,默认反代到 `live-console.service`,并由 `live-edge-watch.service` 持续自愈,控制台无需记端口。 +- 总览:展示硬件、软件、网络、直播业务状态,并给出健康检查、失败原因与下一步操作。 +- 服务:展示 ShellCrash、SRS、Neko、Redroid、ADB、File Browser、Netdata、WebTTY 等服务的安装、运行、端口与控制按钮。 +- YouTube 无人直播:单频道与多频道 Pro 共存。每条 Pro 线路独立保存 URL、推流密钥、PM2 进程、日志、Neko 工作室入口。 +- TikTok 无人直播:视频源转发到设备 HDMI,面板可检测输出设备、预览命令、启动/停止链路。 +- 安卓:Redroid 与真实手机都通过 ADB 识别;Web 面板可看屏、点按、长按、输入、安装 APK、批量执行命令。 +- 网络:ShellCrash 解决单机代理上网,面板展示实时/累计上行下行、代理状态、DNS 与出口连通性。 + +## 源码与部署边界 + +当前说明里提到“核心源码”和“实际部署同步另一个文件夹”分离,后续必须固定一个发布流程: + +- 源码目录只以 `/home/eric/sh/d2ypp2` 为准,实际部署目录不手工改代码。 +- `upgrade-live.sh` 负责从源码构建前端、刷新 systemd 单元、同步配置模板、重启 `live-console` 与 `live-edge`。 +- `/opt/live/config` 是设备上的运行配置中心;源码里的 `live-platform/config/defaults/` 只提供默认模板。 +- 用户数据与账号态必须持久化到 `/opt/live/state`、`/opt/live/logs`、`/opt/live-modules/*`,不能随升级覆盖。 + +## P0:先修可用性闭环 + +### 1. 安卓 Web 画面与触控 + +现状:已有 `/android/screenshot`、`/android/action`、`/android/scrcpy/ws`,前端也有截图轮询和 scrcpy H.264 WebSocket。但在 scrcpy-server、WebCodecs、ADB forward、无线断连、Redroid 启动未完成时,界面容易表现为空白,用户误以为安卓不可控。 + +优化: + +- WebSocket 真流失败时自动降级到 PNG 截图轮询,仍保留点按/双击/长按。 +- 在安卓页显式展示 `adb devices -l`、scrcpy-server jar、WebCodecs、截图接口、触控接口的诊断结果。 +- Redroid 启动后自动 `adb connect 127.0.0.1:5555`,真实手机则提示 USB 授权或无线配对。 +- 触控坐标优先按截图/Canvas 实际像素映射;旋转后重新读取 `wm size` 与截图尺寸。 + +涉及文件: + +- `web-console/components/live-product/LiveAndroidStreamConsole.tsx` +- `src/android_control.py` +- `src/scrcpy_stream.py` +- `src/redroid_control.py` +- `web.py` + +验收: + +- scrcpy-server 不存在或 WebSocket 失败时,安卓页仍能显示截图并可点按。 +- Redroid、USB 手机、无线 ADB 至少各有一条诊断路径。 +- `pytest tests/test_android_redroid.py` 通过,前端 `npm run build` 通过。 + +### 2. Neko 基线与 Google 友好 + +现状:Neko 运行路径已经改为动态生成 compose。`NEKO_INSTANCE_COUNT` 默认保底 2,最大受 `NEKO_MAX_INSTANCE_COUNT` 限制并封顶 64;每个实例都有独立 profile、HTTP/WebRTC 端口、Tampermonkey 强制安装、开发者模式与 IP 变更 watchdog。 + +优化: + +- 保底实例数为 2;多频道 Pro 线路数超过实例数时,保存频道会自动提升 `NEKO_INSTANCE_COUNT` 并后台重启 Neko。 +- 每个实例独立 `NEKO_PROFILE_HOST_DIR_N`,账号态、Tampermonkey、YouTube Studio 页面保持持久化。 +- 新建 profile 会从 1 号健康 profile 自动补种 Tampermonkey 扩展目录、脚本 LevelDB 与 managed settings,避免新频道新 Neko 依赖 Chrome Web Store 实时下载后才出现默认脚本。 +- Neko 默认启用超级管理员入口,管理员密码来自 `NEKO_ADMIN_PASS`,普通观看/操作密码来自 `NEKO_USER_PASS`。 +- 浏览器默认打开开发者模式,并通过 Chrome policy 强制安装 Tampermonkey。 +- 默认使用 Google/YouTube 登录入口,保留完整 ICE,避免更换 Wi-Fi 后 WebRTC 仍指向旧局域网 IP。 +- Tampermonkey 默认脚本继续使用 `services/neko/provisioning/youtube-studio-auto-dismiss.user.js`,来源 URL 固定为 `https://greasyfork.org/en/scripts/557378-youtube-studio-auto-dismiss/code`,渲染 JSON 后由 Chrome policy 导入。 + +涉及文件: + +- `services/neko/docker-compose.yml` +- `tools/render_neko_compose.py` +- `services/neko/browser-launch.sh` +- `services/neko/policies/policies.json` +- `services/neko/provisioning/*` +- `tools/render_tampermonkey_provisioning.py` +- `scripts/linux/neko_ip_watch.sh` +- `scripts/linux/service_ctl.sh` + +验收: + +- 新安装后至少有 `Neko 1`、`Neko 2` 两个入口。 +- `service_ctl.sh neko restart` 会生成 `services/neko/docker-compose.generated.yml`,按 `NEKO_INSTANCE_COUNT` 启动 `live-neko-1..N`。 +- `tools/verify_local_install.py` 会逐实例检查浏览器 supervisor、Tampermonkey provisioning server、默认脚本 payload 与脚本存储落盘。 +- 切换局域网 IP 后 `live-neko-*` 重启并刷新 `NEKO_WEBRTC_NAT1TO1`。 +- Chrome/Chromium 默认开发者模式可见,Tampermonkey 脚本已启用。 + +### 3. YouTube 多频道 Pro + +现状:多频道列表已落盘 SQLite,每条线路可独立生成 `youtube..ini` 与 `URL_config..ini`,PM2 名使用 `youtube2__`;保存频道时会调用 `src/neko_capacity.py` 保证 Neko 容量不小于频道数且最少 2 个。 + +优化: + +- 新建线路保存成功后,自动保证对应配置文件存在,并在进程列表显示该线路。 +- 新建线路保存成功后,自动保证存在对应 Neko 实例,动态 compose 按线路数生成 `Neko 1..N`。 +- 线路和 Neko 实例建立稳定映射:线路 1 -> Neko 1,线路 2 -> Neko 2,线路 N -> Neko N;超过设备上限时进入“需要扩容硬件/降低实例数”的明确状态。 +- 开始直播前校验:抖音 URL、YouTube key、FFmpeg、PM2、SRS/RTMP 端口、Neko 可达性。 +- 日志按线路隔离,面板展示最近错误、推流中/等待上游/密钥异常/心跳过期。 + +涉及文件: + +- `web-console/components/live-product/LiveYoutubeUnmannedView.tsx` +- `web-console/lib/youtubeProChannels.ts` +- `src/youtube_pro_runtime.py` +- `src/hub_kv_store.py` +- `web.py` + +验收: + +- 新增线路保存后刷新页面仍存在。 +- 每条线路的 URL 与 key 不互相覆盖。 +- 未填 key 或 URL 时不能启动,提示具体原因。 + +## P1:补齐设备与网络体验 + +### 4. TikTok HDMI 链路 + +优化: + +- 自动探测 `/dev/dri/card*`、`/dev/fb*`、`/dev/video*`、USB HDMI 采集卡、EDID。 +- 提供 `mpv --drm-*`、`ffmpeg -> kms/drm`、`v4l2loopback` 的候选命令与失败解释。 +- 将视频源、输出接口、音频输出、分辨率、帧率、重试策略写入配置中心。 + +涉及文件: + +- `web-console/components/live-product/LiveTiktokHdmiView.tsx` +- `src/hdmi_control.py` +- `scripts/srt_to_hdmi_drm.example.sh` +- `tiktok/tiktok.py` + +验收: + +- 有 HDMI/DRM 设备时面板显示推荐输出命令。 +- 无设备时显示缺失项,不影响其他模块运行。 + +### 5. 网络与带宽统计 + +优化: + +- 总览和网络页展示每个物理网卡的实时上行/下行、累计 RX/TX、链路速率、在线状态。 +- ShellCrash 页保留配置编辑、启动/停止/重启、导入 YAML,并增加出口 IP / DNS / YouTube 连通性探测。 +- 支持保存“代理健康检查”结果到 dashboard,业务启动前可选择是否强制检查代理。 + +涉及文件: + +- `src/control_plane.py` +- `web-console/components/live-product/LiveControlApp.tsx` +- `web-console/components/OverviewCharts.tsx` +- `live-platform/config/defaults/services/shellcrash.json` + +验收: + +- `/hub/dashboard` 返回接口流量数据。 +- 网络页无需 Netdata 也能看到基本带宽曲线。 + +## P2:工程化与长期维护 + +### 6. 动态 Neko 扩容 + +已实现由配置中心生成 compose: + +- `NEKO_INSTANCE_COUNT=max(2, youtube_pro_lane_count)`。 +- 每个实例自动分配 HTTP、TCPMUX、UDPMUX、UDP_RANGE、profile 目录。 +- `service_ctl.sh neko restart` 使用生成后的 compose 启动所需实例并停掉多余实例。 +- 生成器:`tools/render_neko_compose.py`。 +- 输出文件:`services/neko/docker-compose.generated.yml`;旧 `services/neko/docker-compose.yml` 保留为兼容模板和人工排障参考。 + +### 7. 安装验收矩阵 + +为每次部署固定验收: + +- `sudo bash doctor.sh` +- `sudo bash probe-live.sh` +- `python3 tools/verify_live_console.py` +- `pytest` +- `npm --prefix web-console run build` + +硬件矩阵: + +- x86_64 小主机:Intel/AMD + VAAPI。 +- ARM64 开发板:RK3588 / Raspberry Pi 5 / Armbian。 +- 安卓:Redroid、USB 手机、无线 ADB。 +- 网络:中国大陆单机代理、普通海外网络、换 Wi-Fi 后 mDNS 与 Neko WebRTC。 + +### 8. 面板信息架构 + +保持当前 7 个一级页面: + +- 总览:硬件/软件/业务摘要、健康检查、关键链路。 +- 服务:基础服务安装与运行控制。 +- YouTube 无人直播:单频道、多频道 Pro、Neko 工作室。 +- TikTok 无人直播:HDMI 输出链路。 +- 安卓:Redroid 与真实手机云控。 +- 网络:ShellCrash、带宽、出口连通性。 +- 设置:配置中心、升级、备份恢复。 + +## 实施优先级 + +1. P0-1:安卓真流失败自动降级截图,补诊断。 +2. P0-3:YouTube Pro 新建线路后保证配置、进程、Neko 映射一致。 +3. P0-2:Neko 基线验收,确认 Tampermonkey 脚本、profile、IP watcher。 +4. P1-5:网络带宽统计与代理连通性。 +5. P1-4:TikTok HDMI 探测和启动链路。 +6. P2-6:动态 Neko compose 已落地;后续重点是按硬件资源给出容量建议和超载保护。 + +## 当前代码落点 + +- 一键入口:`install.sh`、`install-all.sh`、`install-core.sh`、`install-live.sh` +- Linux 脚本:`scripts/linux/lib/common.sh`、`scripts/linux/service_ctl.sh` +- 控制平面:`web.py`、`src/control_plane.py`、`src/hub_routes.py` +- 前端面板:`web-console/components/live-product/*` +- Neko:`services/neko/*`、`tools/render_neko_compose.py`、`src/neko_capacity.py` +- 安卓:`src/android_control.py`、`src/scrcpy_stream.py`、`src/redroid_control.py` +- YouTube Pro:`src/youtube_pro_runtime.py`、`web-console/lib/youtubeProChannels.ts` diff --git a/d2ypp2/install.sh b/d2ypp2/install.sh index 7ce0c15..dd8ea86 100755 --- a/d2ypp2/install.sh +++ b/d2ypp2/install.sh @@ -1,4 +1,426 @@ #!/usr/bin/env bash -set -euo pipefail -ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" -exec bash "$ROOT_DIR/install-all.sh" "$@" +# SHD2YPP2 remote bootstrapper. +# +# Supported entrypoints: +# curl -fsSL http:///install.sh | bash +# curl -fsSL http:///install.sh | bash -s -- --profile all +# sudo bash install.sh --profile hub +# +# The heavy OS/service installation stays in scripts/linux/install_stack.sh. +# This file owns the production bootstrap contract: source acquisition, locking, +# logging, privilege handling, profile dispatch, and post-install verification. +set -Eeuo pipefail + +PRODUCT_NAME="SHD2YPP2" +DEFAULT_REPO_URL="http://gitlab.nomadro.com/gitlab-instance-0a899031/sh.git" +DEFAULT_REPO_REF="best" +DEFAULT_REPO_SUBDIR="d2ypp2" +DEFAULT_INSTALL_DIR="/opt/live/d2ypp" +DEFAULT_LOG_DIR="/var/log/shd2ypp2" +DEFAULT_LOCK_FILE="/run/lock/shd2ypp2-install.lock" + +PROFILE="all" +REPO_URL="${LIVE_REPO_URL:-$DEFAULT_REPO_URL}" +REPO_REF="${LIVE_REPO_REF:-$DEFAULT_REPO_REF}" +REPO_SUBDIR="${LIVE_REPO_SUBDIR:-$DEFAULT_REPO_SUBDIR}" +INSTALL_DIR="${LIVE_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" +LOG_DIR="${LIVE_INSTALL_LOG_DIR:-$DEFAULT_LOG_DIR}" +LOCK_FILE="${LIVE_INSTALL_LOCK_FILE:-$DEFAULT_LOCK_FILE}" +VERIFY=1 +LOCAL_ONLY=0 +KEEP_SOURCE=0 +ASSUME_YES=1 + +SELF_PATH="${BASH_SOURCE[0]:-$0}" +START_TS="$(date +%Y%m%d-%H%M%S)" +LOG_FILE="$LOG_DIR/install-$START_TS.log" + +log() { + printf '[install] %s\n' "$*" >&2 +} + +die() { + log "ERROR: $*" + exit 1 +} + +usage() { + cat </install.sh | bash + bash install.sh [options] + +Options: + --profile all|hub|core|business|live Install profile. Default: all + --repo URL Git repository URL. Default: $DEFAULT_REPO_URL + --ref REF Git branch/tag/commit. Default: $DEFAULT_REPO_REF + --subdir PATH Project subdir inside repo. Default: $DEFAULT_REPO_SUBDIR + --dir PATH Runtime source directory. Default: $DEFAULT_INSTALL_DIR + --no-verify Skip post-install health probes + --local-only Require running from an existing project checkout + --keep-source Keep temporary clone directory for debugging + -h, --help Show this help + +Environment overrides: + LIVE_REPO_URL, LIVE_REPO_REF, LIVE_REPO_SUBDIR, LIVE_INSTALL_DIR + HOSTNAME_ALIAS, PORT and other config/system-stack.env values +USAGE +} + +parse_args() { + while [ "$#" -gt 0 ]; do + case "$1" in + --profile) + PROFILE="${2:-}" + shift 2 + ;; + --profile=*) + PROFILE="${1#*=}" + shift + ;; + --repo) + REPO_URL="${2:-}" + shift 2 + ;; + --repo=*) + REPO_URL="${1#*=}" + shift + ;; + --ref) + REPO_REF="${2:-}" + shift 2 + ;; + --ref=*) + REPO_REF="${1#*=}" + shift + ;; + --subdir) + REPO_SUBDIR="${2:-}" + shift 2 + ;; + --subdir=*) + REPO_SUBDIR="${1#*=}" + shift + ;; + --dir) + INSTALL_DIR="${2:-}" + shift 2 + ;; + --dir=*) + INSTALL_DIR="${1#*=}" + shift + ;; + --no-verify) + VERIFY=0 + shift + ;; + --local-only) + LOCAL_ONLY=1 + shift + ;; + --keep-source) + KEEP_SOURCE=1 + shift + ;; + -y|--yes) + ASSUME_YES=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "Unknown argument: $1" + ;; + esac + done + + case "$PROFILE" in + all|full) PROFILE="all" ;; + hub|core|sh) PROFILE="hub" ;; + business|live|app|sh2) PROFILE="business" ;; + *) die "Unsupported profile: $PROFILE" ;; + esac + [ -n "$REPO_URL" ] || die "--repo cannot be empty" + [ -n "$REPO_REF" ] || die "--ref cannot be empty" + [ -n "$INSTALL_DIR" ] || die "--dir cannot be empty" +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +root_prefix() { + if [ "$(id -u)" -eq 0 ]; then + return 0 + fi + if ! have sudo; then + die "sudo is required when not running as root. Use: curl -fsSL /install.sh | sudo -E bash" + fi + sudo -n true 2>/dev/null || sudo -v +} + +run_root() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + else + sudo -E "$@" + fi +} + +run_root_shell() { + if [ "$(id -u)" -eq 0 ]; then + bash -lc "$*" + else + sudo -E bash -lc "$*" + fi +} + +setup_logging() { + root_prefix + run_root mkdir -p "$LOG_DIR" "$(dirname "$LOCK_FILE")" + run_root chmod 0755 "$LOG_DIR" + if [ "$(id -u)" -eq 0 ]; then + exec > >(tee -a "$LOG_FILE") 2>&1 + else + exec > >(sudo -E tee -a "$LOG_FILE") 2>&1 + fi + log "$PRODUCT_NAME bootstrap started at $START_TS" + log "Log file: $LOG_FILE" +} + +detect_platform() { + [ -r /etc/os-release ] || die "/etc/os-release not found; Debian/Ubuntu/Armbian are supported" + # shellcheck source=/dev/null + . /etc/os-release + local family="${ID:-} ${ID_LIKE:-}" + if ! printf '%s' "$family" | grep -qiE 'debian|ubuntu'; then + die "Unsupported distribution: ${PRETTY_NAME:-unknown}. Use Debian, Ubuntu, Armbian, Raspberry Pi OS, or a compatible derivative." + fi + local arch + arch="$(uname -m 2>/dev/null || true)" + case "$arch" in + x86_64|amd64|aarch64|arm64|armv7l|armv6l) ;; + *) log "WARN: untested architecture: ${arch:-unknown}; continuing with best-effort install" ;; + esac + log "Platform: ${PRETTY_NAME:-$family}; arch=$arch; kernel=$(uname -r)" +} + +apt_install_bootstrap_deps() { + local need_apt=0 + for cmd in git curl rsync flock; do + have "$cmd" || need_apt=1 + done + [ "$need_apt" = 1 ] || return 0 + log "Installing bootstrap dependencies" + run_root env DEBIAN_FRONTEND=noninteractive apt-get update + run_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates curl git rsync util-linux sudo +} + +acquire_lock() { + run_root touch "$LOCK_FILE" + if [ "$(id -u)" -ne 0 ]; then + run_root chown "$(id -u):$(id -g)" "$LOCK_FILE" + fi + exec 9>"$LOCK_FILE" + if ! flock -n 9; then + die "Another installer is already running. Lock: $LOCK_FILE" + fi +} + +is_project_dir() { + local dir="$1" + [ -f "$dir/scripts/linux/install_stack.sh" ] && [ -f "$dir/web.py" ] && [ -d "$dir/web-console" ] +} + +local_project_dir() { + local candidate + if [ -n "$SELF_PATH" ] && [ -f "$SELF_PATH" ]; then + candidate="$(cd "$(dirname "$SELF_PATH")" && pwd)" + if is_project_dir "$candidate"; then + printf '%s\n' "$candidate" + return 0 + fi + fi + candidate="$(pwd)" + if is_project_dir "$candidate"; then + printf '%s\n' "$candidate" + return 0 + fi + return 1 +} + +clone_source() { + local clone_root project_dir + clone_root="/opt/live/src/shd2ypp2-$START_TS" + log "Fetching source: repo=$REPO_URL ref=$REPO_REF subdir=$REPO_SUBDIR" + run_root mkdir -p "$(dirname "$clone_root")" + run_root git clone --depth 1 --branch "$REPO_REF" "$REPO_URL" "$clone_root" || { + log "Shallow clone failed; retrying full fetch" + run_root rm -rf "$clone_root" + run_root git clone "$REPO_URL" "$clone_root" + run_root git -C "$clone_root" checkout "$REPO_REF" + } + if [ -n "$REPO_SUBDIR" ]; then + project_dir="$clone_root/$REPO_SUBDIR" + else + project_dir="$clone_root" + fi + is_project_dir "$project_dir" || die "Fetched source does not contain SHD2YPP2 project at: $project_dir" + printf '%s\n' "$project_dir" +} + +sync_to_install_dir() { + local source_dir="$1" + local target="$INSTALL_DIR" + log "Syncing source to $target" + run_root mkdir -p "$target" + run_root rsync -a --delete \ + --exclude '.git/' \ + --exclude '.venv/' \ + --exclude '.next/' \ + --exclude 'node_modules/' \ + --exclude 'web-console/node_modules/' \ + --exclude 'web-console/.next/' \ + --exclude 'logs/' \ + --exclude 'downloads/' \ + --exclude 'backup_config/' \ + --exclude '.pm2/' \ + --exclude '.process_runner/' \ + --exclude 'services/neko/data/' \ + --exclude 'config/system-stack.env' \ + "$source_dir/" "$target/" + run_root chown -R live:live "$target" 2>/dev/null || true + printf '%s\n' "$target" +} + +resolve_project_dir() { + local project="" + if project="$(local_project_dir 2>/dev/null)"; then + log "Using local project checkout: $project" + project="$(sync_to_install_dir "$project")" + printf '%s\n' "$project" + return 0 + fi + if [ "$LOCAL_ONLY" = 1 ]; then + die "--local-only was set, but current directory is not a SHD2YPP2 checkout" + fi + apt_install_bootstrap_deps + local fetched + fetched="$(clone_source)" + sync_to_install_dir "$fetched" + if [ "$KEEP_SOURCE" != 1 ]; then + run_root rm -rf "$(dirname "$fetched")" + else + log "Keeping fetched source at $(dirname "$fetched")" + fi +} + +install_profile() { + local project_dir="$1" + local stack="$project_dir/scripts/linux/install_stack.sh" + [ -f "$stack" ] || die "Missing installer: $stack" + log "Dispatching install profile: $PROFILE" + run_root chmod +x "$stack" "$project_dir"/install-*.sh "$project_dir"/start.sh 2>/dev/null || true + run_root env \ + LIVE_STACK_REPO_ROOT="$INSTALL_DIR" \ + LIVE_INSTALL_LOG_FILE="$LOG_FILE" \ + ASSUME_YES="$ASSUME_YES" \ + bash "$stack" "$PROFILE" +} + +post_verify() { + [ "$VERIFY" = 1 ] || { + log "Post-install verification skipped by --no-verify" + return 0 + } + local port="${PORT:-8001}" + if [ -f "$INSTALL_DIR/config/system-stack.env" ]; then + # shellcheck source=/dev/null + . "$INSTALL_DIR/config/system-stack.env" || true + port="${PORT:-8001}" + fi + log "Waiting for control plane on 127.0.0.1:$port" + local ok=0 i + for i in $(seq 1 90); do + if curl -fsS -m 3 "http://127.0.0.1:$port/health" >/dev/null 2>&1; then + ok=1 + break + fi + sleep 2 + done + if [ "$ok" != 1 ]; then + run_root journalctl -u live-console.service -n 120 --no-pager || true + die "Control plane did not become healthy on 127.0.0.1:$port" + fi + log "Control plane health OK" + + if curl -fsS -m 8 "http://127.0.0.1:$port/deploy/check" >/tmp/shd2ypp2-deploy-check.json 2>/dev/null; then + python3 - <<'PY' +from __future__ import annotations + +import json +from pathlib import Path + +data = json.loads(Path("/tmp/shd2ypp2-deploy-check.json").read_text(encoding="utf-8")) +status = "READY" if data.get("ready") else "DEGRADED" +print(f"[install] deploy/check: {status} ({data.get('pass_count', 0)}/{data.get('total_count', 0)})") +for item in data.get("failing_checks") or []: + label = item.get("label") or item.get("id") or "check" + detail = item.get("detail") or item.get("error") or "" + print(f"[install] FAIL {label}: {detail}") +for hint in data.get("hints") or []: + print(f"[install] hint: {hint}") +PY + else + log "WARN: /deploy/check unavailable; /health passed" + fi + + local verifier="$INSTALL_DIR/tools/verify_local_install.py" + if [ -f "$verifier" ]; then + log "Running local production verifier" + run_root chmod +x "$verifier" 2>/dev/null || true + run_root python3 "$verifier" --install-dir "$INSTALL_DIR" --timeout 6 + else + log "WARN: local verifier not found at $verifier" + fi +} + +print_finish() { + local host_alias="${HOSTNAME_ALIAS:-live}" + local port="${PORT:-8001}" + if [ -f "$INSTALL_DIR/config/system-stack.env" ]; then + # shellcheck source=/dev/null + . "$INSTALL_DIR/config/system-stack.env" || true + host_alias="${HOSTNAME_ALIAS:-live}" + port="${PORT:-8001}" + fi + cat < None: else: host = args.host or os.environ.get("HOST") or "0.0.0.0" + os.environ["HOST"] = host + os.environ["PORT"] = port + need_npm = bool(args.dev or args.build_only or (not args.skip_build and not args.api_only and _console_needs_rebuild())) if not args.no_env_check: env_check.print_env_report( diff --git a/d2ypp2/scripts/linux/install_all.sh b/d2ypp2/scripts/linux/install_all.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/install_business.sh b/d2ypp2/scripts/linux/install_business.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/install_hub.sh b/d2ypp2/scripts/linux/install_hub.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/install_stack.sh b/d2ypp2/scripts/linux/install_stack.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/launch_browser.sh b/d2ypp2/scripts/linux/launch_browser.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/lib/common.sh b/d2ypp2/scripts/linux/lib/common.sh old mode 100644 new mode 100755 index ae03bc6..e7c480a --- a/d2ypp2/scripts/linux/lib/common.sh +++ b/d2ypp2/scripts/linux/lib/common.sh @@ -487,10 +487,15 @@ redroid_container_matches_host_support() { resolve_neko_instance_count() { local requested="${NEKO_INSTANCE_COUNT:-2}" - local cpu count + local cpu count max_count + max_count="${NEKO_MAX_INSTANCE_COUNT:-64}" + case "$max_count" in + ''|*[!0-9]*) max_count=64 ;; + esac + [ "$max_count" -lt 2 ] && max_count=2 + [ "$max_count" -gt 64 ] && max_count=64 cpu="$(nproc 2>/dev/null || echo 4)" case "$requested" in - 1|2|3) count="$requested" ;; auto|"") if [ "$cpu" -le 4 ]; then count=2 @@ -498,15 +503,16 @@ resolve_neko_instance_count() { count=3 fi ;; + *[!0-9]*) count=2 ;; *) - count=2 + count="$requested" ;; esac - if [ "$count" -lt 1 ]; then - count=1 + if [ "$count" -lt 2 ]; then + count=2 fi - if [ "$count" -gt 3 ]; then - count=3 + if [ "$count" -gt "$max_count" ]; then + count="$max_count" fi printf '%s' "$count" } @@ -514,7 +520,7 @@ resolve_neko_instance_count() { resolve_neko_service_names() { local count name count="$(resolve_neko_instance_count)" - for name in 1 2 3; do + for name in $(seq 1 "$count"); do [ "$name" -le "$count" ] || break printf 'neko%s\n' "$name" done @@ -524,13 +530,85 @@ resolve_neko_instances_csv() { resolve_neko_service_names | paste -sd, - } +seed_neko_tampermonkey_profile() { + local target="${1:-}" donor="${2:-}" rel src dst + [ -n "$target" ] || return 0 + [ -n "$donor" ] || donor="$(resolve_neko_profile_dir_for_instance 1)" + [ "$target" = "$donor" ] && return 0 + [ -d "$donor/Default" ] || return 0 + mkdir -p "$target/Default" + for rel in \ + "Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" \ + "Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo" \ + "Managed Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo"; do + src="$donor/Default/$rel" + dst="$target/Default/$rel" + [ -e "$src" ] || continue + [ ! -e "$dst" ] || continue + mkdir -p "$(dirname "$dst")" + cp -a "$src" "$dst" 2>/dev/null || true + done +} + resolve_neko_http_port_for_instance() { - case "${1:-1}" in - 1) printf '%s' "${NEKO_HTTP_PORT_1:-${NEKO_HTTP_PORT:-9200}}" ;; - 2) printf '%s' "${NEKO_HTTP_PORT_2:-9201}" ;; - 3) printf '%s' "${NEKO_HTTP_PORT_3:-9202}" ;; - *) printf '%s' "${NEKO_HTTP_PORT_1:-${NEKO_HTTP_PORT:-9200}}" ;; + local idx="${1:-1}" value base + eval "value=\"\${NEKO_HTTP_PORT_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_HTTP_PORT:-9200}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_tcpmux_for_instance() { + local idx="${1:-1}" value base + eval "value=\"\${NEKO_WEBRTC_TCPMUX_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_WEBRTC_TCPMUX:-52300}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_udpmux_for_instance() { + local idx="${1:-1}" value base + eval "value=\"\${NEKO_WEBRTC_UDPMUX_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_WEBRTC_UDPMUX:-52300}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_udp_range_for_instance() { + local idx="${1:-1}" value start + eval "value=\"\${NEKO_WEBRTC_UDP_RANGE_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + if [ "$idx" -eq 1 ] && [ -n "${NEKO_WEBRTC_UDP_RANGE:-}" ]; then + printf '%s' "$NEKO_WEBRTC_UDP_RANGE" + return 0 + fi + case "$idx" in + 2) start=52101 ;; + 3) start=52201 ;; + *) start=$((52400 + ((idx - 4) * 100))) ;; esac + [ "$idx" -eq 1 ] && start=52000 + printf '%s-%s' "$start" "$((start + 99))" +} + +render_neko_compose_file() { + local count out + count="$(resolve_neko_instance_count)" + out="$ROOT_DIR/services/neko/docker-compose.generated.yml" + python3 "$ROOT_DIR/tools/render_neko_compose.py" --count "$count" --output "$out" + printf '%s' "$out" } remove_legacy_neko_container() { @@ -1137,16 +1215,12 @@ install_neko_browser() { log "Neko skipped: docker compose unavailable" return 0 fi - log "Installing Neko browser desktops (Docker, 3 instances)" + log "Installing Neko browser desktops (Docker, dynamic instances)" set -a [ -f "$STACK_ENV" ] && . "$STACK_ENV" set +a - local neko_dir="$ROOT_DIR/services/neko" - local idx neko_prof - for idx in 1 2 3; do - neko_prof="$(resolve_neko_profile_dir_for_instance "$idx")" - prune_neko_profile "$neko_prof" - done + local idx neko_prof instance_count port mux udp range compose_file + instance_count="$(resolve_neko_instance_count)" export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" export NEKO_HTTP_PORT_1="${NEKO_HTTP_PORT_1:-$NEKO_HTTP_PORT}" export NEKO_HTTP_PORT_2="${NEKO_HTTP_PORT_2:-9201}" @@ -1190,17 +1264,22 @@ install_neko_browser() { export NEKO_WEBRTC_NAT1TO1 fi export NEKO_PLUGINS_ENABLED="${NEKO_PLUGINS_ENABLED:-true}" - for port in "$NEKO_HTTP_PORT_1" "$NEKO_HTTP_PORT_2" "$NEKO_HTTP_PORT_3" "$NEKO_WEBRTC_TCPMUX_1" "$NEKO_WEBRTC_TCPMUX_2" "$NEKO_WEBRTC_TCPMUX_3"; do + for idx in $(seq 1 "$instance_count"); do + neko_prof="$(resolve_neko_profile_dir_for_instance "$idx")" + seed_neko_tampermonkey_profile "$neko_prof" "$(resolve_neko_profile_dir_for_instance 1)" + prune_neko_profile "$neko_prof" + port="$(resolve_neko_http_port_for_instance "$idx")" + mux="$(resolve_neko_tcpmux_for_instance "$idx")" + udp="$(resolve_neko_udpmux_for_instance "$idx")" + range="$(resolve_neko_udp_range_for_instance "$idx")" allow_tcp_port_if_firewall_active "$port" - done - for port in "$NEKO_WEBRTC_UDPMUX_1" "$NEKO_WEBRTC_UDPMUX_2" "$NEKO_WEBRTC_UDPMUX_3"; do - allow_udp_port_if_firewall_active "$port" - done - for port_range in "$NEKO_WEBRTC_UDP_RANGE_1" "$NEKO_WEBRTC_UDP_RANGE_2" "$NEKO_WEBRTC_UDP_RANGE_3"; do - allow_udp_range_if_firewall_active "$port_range" + allow_tcp_port_if_firewall_active "$mux" + allow_udp_port_if_firewall_active "$udp" + allow_udp_range_if_firewall_active "$range" done remove_legacy_neko_container - compose_in_service_dir "$neko_dir/docker-compose.yml" up -d + compose_file="$(render_neko_compose_file)" + compose_in_service_dir "$compose_file" up -d } install_redroid() { @@ -1945,11 +2024,33 @@ ExecStart=$caddy_bin run --config /opt/live/generated/Caddyfile --adapter caddyf Restart=on-failure RestartSec=4 +[Install] +WantedBy=multi-user.target +UNIT + + chmod +x "$ROOT_DIR/scripts/linux/live_edge_watch.sh" 2>/dev/null || true + cat >/etc/systemd/system/live-edge-watch.service </dev/null; then + log "live-console.service inactive; starting" + systemctl start live-console.service >/dev/null 2>&1 || true + sleep 2 + fi + + if ! systemctl is-active --quiet live-edge.service 2>/dev/null; then + log "live-edge.service inactive; starting" + systemctl start live-edge.service >/dev/null 2>&1 || true + sleep "$INTERVAL" + continue + fi + + if ! curl -fsS -m 4 "http://127.0.0.1/health" >/dev/null 2>&1; then + log "port 80 health failed; restarting live-edge.service for upstream :$PORT" + systemctl restart live-edge.service >/dev/null 2>&1 || true + fi + + sleep "$INTERVAL" +done diff --git a/d2ypp2/scripts/linux/neko_ip_watch.sh b/d2ypp2/scripts/linux/neko_ip_watch.sh old mode 100644 new mode 100755 index 15f3e17..93cca18 --- a/d2ypp2/scripts/linux/neko_ip_watch.sh +++ b/d2ypp2/scripts/linux/neko_ip_watch.sh @@ -31,10 +31,8 @@ normalize_interval() { running_neko_nat1to1_values() { local name value - for name in live-neko-1 live-neko-2 live-neko-3; do - if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then - continue - fi + docker ps --format '{{.Names}}' 2>/dev/null | grep -E '^live-neko-[0-9]+$' | sort -V | while read -r name; do + [ -n "$name" ] || continue value="$( docker inspect "$name" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | awk -F= '/^NEKO_WEBRTC_NAT1TO1=/{print $2; exit}' @@ -51,7 +49,7 @@ maybe_refresh_neko_for_ip_change() { if [ "${ENABLE_NEKO:-1}" != "1" ] || ! have docker; then return 0 fi - if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -Eq '^live-neko-[123]$'; then + if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -Eq '^live-neko-[0-9]+$'; then return 0 fi diff --git a/d2ypp2/scripts/linux/probe_live_access.sh b/d2ypp2/scripts/linux/probe_live_access.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/refresh_live_console_unit.sh b/d2ypp2/scripts/linux/refresh_live_console_unit.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/reinstall_live_edge.sh b/d2ypp2/scripts/linux/reinstall_live_edge.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/run_kmscube.sh b/d2ypp2/scripts/linux/run_kmscube.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/service_ctl.sh b/d2ypp2/scripts/linux/service_ctl.sh old mode 100644 new mode 100755 index 289f124..ef684bd --- a/d2ypp2/scripts/linux/service_ctl.sh +++ b/d2ypp2/scripts/linux/service_ctl.sh @@ -290,20 +290,50 @@ resolve_neko_profile_dir() { resolve_neko_profile_dir_for_instance() { local idx="${1:-1}" - local base + local base override base="${NEKO_PROFILE_HOST_DIR:-$(resolve_neko_profile_dir)}" + eval "override=\"\${NEKO_PROFILE_HOST_DIR_${idx}:-}\"" + if [ -n "$override" ]; then + printf '%s' "$override" + return 0 + fi case "$idx" in 1) printf '%s' "$base" ;; *) printf '%s-%s' "$base" "$idx" ;; esac } +seed_neko_tampermonkey_profile() { + local target="${1:-}" donor="${2:-}" rel src dst + [ -n "$target" ] || return 0 + [ -n "$donor" ] || donor="$(resolve_neko_profile_dir_for_instance 1)" + [ "$target" = "$donor" ] && return 0 + [ -d "$donor/Default" ] || return 0 + mkdir -p "$target/Default" + for rel in \ + "Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo" \ + "Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo" \ + "Managed Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo"; do + src="$donor/Default/$rel" + dst="$target/Default/$rel" + [ -e "$src" ] || continue + [ ! -e "$dst" ] || continue + mkdir -p "$(dirname "$dst")" + cp -a "$src" "$dst" 2>/dev/null || true + done +} + resolve_neko_instance_count() { local requested="${NEKO_INSTANCE_COUNT:-2}" - local cpu count + local cpu count max_count + max_count="${NEKO_MAX_INSTANCE_COUNT:-64}" + case "$max_count" in + ''|*[!0-9]*) max_count=64 ;; + esac + [ "$max_count" -lt 2 ] && max_count=2 + [ "$max_count" -gt 64 ] && max_count=64 cpu="$(nproc 2>/dev/null || echo 4)" case "$requested" in - 1|2|3) count="$requested" ;; auto|"") if [ "$cpu" -le 4 ]; then count=2 @@ -311,19 +341,81 @@ resolve_neko_instance_count() { count=3 fi ;; + *[!0-9]*) count=2 ;; *) - count=2 + count="$requested" ;; esac - if [ "$count" -lt 1 ]; then - count=1 + if [ "$count" -lt 2 ]; then + count=2 fi - if [ "$count" -gt 3 ]; then - count=3 + if [ "$count" -gt "$max_count" ]; then + count="$max_count" fi printf '%s' "$count" } +resolve_neko_http_port_for_instance() { + local idx="${1:-1}" value base + eval "value=\"\${NEKO_HTTP_PORT_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_HTTP_PORT:-9200}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_tcpmux_for_instance() { + local idx="${1:-1}" value base + eval "value=\"\${NEKO_WEBRTC_TCPMUX_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_WEBRTC_TCPMUX:-52300}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_udpmux_for_instance() { + local idx="${1:-1}" value base + eval "value=\"\${NEKO_WEBRTC_UDPMUX_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + base="${NEKO_WEBRTC_UDPMUX:-52300}" + printf '%s' $((base + idx - 1)) +} + +resolve_neko_udp_range_for_instance() { + local idx="${1:-1}" value start + eval "value=\"\${NEKO_WEBRTC_UDP_RANGE_${idx}:-}\"" + if [ -n "$value" ]; then + printf '%s' "$value" + return 0 + fi + if [ "$idx" -eq 1 ] && [ -n "${NEKO_WEBRTC_UDP_RANGE:-}" ]; then + printf '%s' "$NEKO_WEBRTC_UDP_RANGE" + return 0 + fi + case "$idx" in + 2) start=52101 ;; + 3) start=52201 ;; + *) start=$((52400 + ((idx - 4) * 100))) ;; + esac + [ "$idx" -eq 1 ] && start=52000 + printf '%s-%s' "$start" "$((start + 99))" +} + +render_neko_compose_file() { + local count out + count="$(resolve_neko_instance_count)" + out="$ROOT_DIR/services/neko/docker-compose.generated.yml" + python3 "$ROOT_DIR/tools/render_neko_compose.py" --count "$count" --output "$out" + printf '%s' "$out" +} + remove_legacy_neko_container() { if have docker && docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx 'live-neko-chromium'; then docker stop live-neko-chromium >/dev/null 2>&1 || true @@ -884,20 +976,18 @@ service_neko_status() { fi kv available 1 local state - local running_count=0 total_count=0 states="" name instance_count + local running_count=0 total_count=0 states="" name instance_count idx ports="" profiles="" instance_count="$(resolve_neko_instance_count)" total_count="$instance_count" - for name in live-neko-1 live-neko-2 live-neko-3; do - case "$name" in - live-neko-1) [ "$instance_count" -ge 1 ] || continue ;; - live-neko-2) [ "$instance_count" -ge 2 ] || continue ;; - live-neko-3) [ "$instance_count" -ge 3 ] || continue ;; - esac + for idx in $(seq 1 "$instance_count"); do + name="live-neko-$idx" state="$(docker_container_status "$name" || true)" if echo "$state" | grep -qi running; then running_count=$((running_count + 1)) fi states="${states}${name}:${state:-missing};" + ports="${ports}$(resolve_neko_http_port_for_instance "$idx")," + profiles="${profiles}$(resolve_neko_profile_dir_for_instance "$idx")," done if [ "$running_count" -eq 0 ]; then state="$(docker_container_status live-neko-chromium || true)" @@ -915,7 +1005,7 @@ service_neko_status() { if ! neko_nat1to1_is_usable "${NEKO_WEBRTC_NAT1TO1:-}"; then NEKO_WEBRTC_NAT1TO1="$(detect_preferred_lan_ip || true)" fi - kv detail "image=$(resolve_neko_image); supervisor_conf=$(resolve_neko_supervisor_conf); start_url=${NEKO_START_URL}; nat1to1=${NEKO_WEBRTC_NAT1TO1:-unset}; instances=${running_count}/${total_count}; ports=${NEKO_HTTP_PORT_1},${NEKO_HTTP_PORT_2},${NEKO_HTTP_PORT_3}; profiles=$(resolve_neko_profile_dir_for_instance 1),$(resolve_neko_profile_dir_for_instance 2),$(resolve_neko_profile_dir_for_instance 3); screen=${NEKO_DESKTOP_SCREEN}; shm=${NEKO_SHM_SIZE}; fps=${NEKO_MAX_FPS}; bitrate=${NEKO_VIDEO_BITRATE}; render=${NEKO_BROWSER_RENDER_MODE}; udp_ranges=${NEKO_WEBRTC_UDP_RANGE_1},${NEKO_WEBRTC_UDP_RANGE_2},${NEKO_WEBRTC_UDP_RANGE_3}; tcpmux=${NEKO_WEBRTC_TCPMUX_1},${NEKO_WEBRTC_TCPMUX_2},${NEKO_WEBRTC_TCPMUX_3}; udpmux=${NEKO_WEBRTC_UDPMUX_1},${NEKO_WEBRTC_UDPMUX_2},${NEKO_WEBRTC_UDPMUX_3}; containers=${states}" + kv detail "image=$(resolve_neko_image); supervisor_conf=$(resolve_neko_supervisor_conf); start_url=${NEKO_START_URL}; nat1to1=${NEKO_WEBRTC_NAT1TO1:-unset}; instances=${running_count}/${total_count}; ports=${ports%,}; profiles=${profiles%,}; screen=${NEKO_DESKTOP_SCREEN}; shm=${NEKO_SHM_SIZE}; fps=${NEKO_MAX_FPS}; bitrate=${NEKO_VIDEO_BITRATE}; render=${NEKO_BROWSER_RENDER_MODE}; containers=${states}" } service_neko_start() { @@ -923,77 +1013,32 @@ service_neko_start() { kv message "请先在 config/system-stack.env 设置 ENABLE_NEKO=1" exit 1 fi - if [ ! -f "$NEKO_COMPOSE" ]; then - kv message "Neko compose missing" + if [ ! -f "$ROOT_DIR/tools/render_neko_compose.py" ]; then + kv message "Neko compose renderer missing" return 0 fi - local prof + local prof idx port mux udp range compose_file service_names="" local instance_count - local service_names="" extra_service_names="" idx_name instance_count="$(resolve_neko_instance_count)" - for idx_name in 1 2 3; do - case "$idx_name" in - 1) - if [ "$instance_count" -ge 1 ]; then - service_names="${service_names} neko1" - else - extra_service_names="${extra_service_names} neko1" - fi - ;; - 2) - if [ "$instance_count" -ge 2 ]; then - service_names="${service_names} neko2" - else - extra_service_names="${extra_service_names} neko2" - fi - ;; - 3) - if [ "$instance_count" -ge 3 ]; then - service_names="${service_names} neko3" - else - extra_service_names="${extra_service_names} neko3" - fi - ;; - esac - done - for idx in 1 2 3; do - [ "$idx" -le "$instance_count" ] || break - prof="$(resolve_neko_profile_dir_for_instance "$idx")" - prune_neko_profile "$prof" - done export_neko_env - for port in "$NEKO_HTTP_PORT_1" "$NEKO_HTTP_PORT_2" "$NEKO_HTTP_PORT_3" "$NEKO_WEBRTC_TCPMUX_1" "$NEKO_WEBRTC_TCPMUX_2" "$NEKO_WEBRTC_TCPMUX_3"; do - case "$port" in - "$NEKO_HTTP_PORT_1"|"$NEKO_WEBRTC_TCPMUX_1") [ "$instance_count" -ge 1 ] || continue ;; - "$NEKO_HTTP_PORT_2"|"$NEKO_WEBRTC_TCPMUX_2") [ "$instance_count" -ge 2 ] || continue ;; - "$NEKO_HTTP_PORT_3"|"$NEKO_WEBRTC_TCPMUX_3") [ "$instance_count" -ge 3 ] || continue ;; - esac + for idx in $(seq 1 "$instance_count"); do + prof="$(resolve_neko_profile_dir_for_instance "$idx")" + seed_neko_tampermonkey_profile "$prof" "$(resolve_neko_profile_dir_for_instance 1)" + prune_neko_profile "$prof" + port="$(resolve_neko_http_port_for_instance "$idx")" + mux="$(resolve_neko_tcpmux_for_instance "$idx")" + udp="$(resolve_neko_udpmux_for_instance "$idx")" + range="$(resolve_neko_udp_range_for_instance "$idx")" allow_tcp_port_if_firewall_active "$port" - done - for port in "$NEKO_WEBRTC_UDPMUX_1" "$NEKO_WEBRTC_UDPMUX_2" "$NEKO_WEBRTC_UDPMUX_3"; do - case "$port" in - "$NEKO_WEBRTC_UDPMUX_1") [ "$instance_count" -ge 1 ] || continue ;; - "$NEKO_WEBRTC_UDPMUX_2") [ "$instance_count" -ge 2 ] || continue ;; - "$NEKO_WEBRTC_UDPMUX_3") [ "$instance_count" -ge 3 ] || continue ;; - esac - allow_udp_port_if_firewall_active "$port" - done - for port_range in "$NEKO_WEBRTC_UDP_RANGE_1" "$NEKO_WEBRTC_UDP_RANGE_2" "$NEKO_WEBRTC_UDP_RANGE_3"; do - case "$port_range" in - "$NEKO_WEBRTC_UDP_RANGE_1") [ "$instance_count" -ge 1 ] || continue ;; - "$NEKO_WEBRTC_UDP_RANGE_2") [ "$instance_count" -ge 2 ] || continue ;; - "$NEKO_WEBRTC_UDP_RANGE_3") [ "$instance_count" -ge 3 ] || continue ;; - esac - allow_udp_range_if_firewall_active "$port_range" + allow_tcp_port_if_firewall_active "$mux" + allow_udp_port_if_firewall_active "$udp" + allow_udp_range_if_firewall_active "$range" + service_names="${service_names} neko${idx}" done remove_legacy_neko_container - # 只启动实际需要的实例,避免多余的 Chrome/Xorg 进程继续吃 CPU + compose_file="$(render_neko_compose_file)" # shellcheck disable=SC2086 - compose_in_service_dir "$NEKO_COMPOSE" up -d $service_names - if [ -n "${extra_service_names# }" ]; then - # shellcheck disable=SC2086 - compose_in_service_dir "$NEKO_COMPOSE" stop $extra_service_names >/dev/null 2>&1 || true - fi + compose_in_service_dir "$compose_file" up -d $service_names kv message "Neko stack started (${instance_count} browser instances)" } @@ -1049,12 +1094,14 @@ export_neko_env() { } service_neko_stop() { - if [ ! -f "$NEKO_COMPOSE" ]; then + if [ ! -f "$ROOT_DIR/tools/render_neko_compose.py" ]; then kv message "Neko compose missing" return 0 fi + local compose_file export_neko_env - compose_in_service_dir "$NEKO_COMPOSE" down + compose_file="$(render_neko_compose_file)" + compose_in_service_dir "$compose_file" down --remove-orphans kv message "Neko stack stopped" } diff --git a/d2ypp2/scripts/linux/uninstall_stack.sh b/d2ypp2/scripts/linux/uninstall_stack.sh old mode 100644 new mode 100755 index c28ce3b..ec8ecfd --- a/d2ypp2/scripts/linux/uninstall_stack.sh +++ b/d2ypp2/scripts/linux/uninstall_stack.sh @@ -9,7 +9,7 @@ ensure_root "$@" load_env log "Stopping systemd services" -for svc in live-edge.service live-console.service live-neko-ip-watch.service live-webtty.service live-android-panel.service live-pulseaudio.service live-kmscube.service live-redroid-host.service netdata cockpit.socket shellcrash.service avahi-daemon; do +for svc in live-edge-watch.service live-edge.service live-console.service live-neko-ip-watch.service live-webtty.service live-android-panel.service live-pulseaudio.service live-kmscube.service live-redroid-host.service netdata cockpit.socket shellcrash.service avahi-daemon; do systemctl stop "$svc" >/dev/null 2>&1 || true systemctl disable "$svc" >/dev/null 2>&1 || true done @@ -41,6 +41,7 @@ fi log "Removing systemd unit files" rm -f /etc/systemd/system/live-edge.service +rm -f /etc/systemd/system/live-edge-watch.service rm -f /etc/systemd/system/live-console.service rm -f /etc/systemd/system/live-neko-ip-watch.service rm -f /etc/systemd/system/live-webtty.service diff --git a/d2ypp2/scripts/linux/upgrade_ffmpeg_newer.sh b/d2ypp2/scripts/linux/upgrade_ffmpeg_newer.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/upgrade_live_console.sh b/d2ypp2/scripts/linux/upgrade_live_console.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/linux/webtty_shell.sh b/d2ypp2/scripts/linux/webtty_shell.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/scripts/srt_to_hdmi_drm.example.sh b/d2ypp2/scripts/srt_to_hdmi_drm.example.sh old mode 100644 new mode 100755 diff --git a/d2ypp2/services/neko/browser-launch.sh b/d2ypp2/services/neko/browser-launch.sh old mode 100644 new mode 100755 index 5190cc1..e0d534d --- a/d2ypp2/services/neko/browser-launch.sh +++ b/d2ypp2/services/neko/browser-launch.sh @@ -128,7 +128,6 @@ launch_browser() { --force-dark-mode \ --disable-background-mode \ --disable-dev-shm-usage \ - --disable-background-networking \ --disable-breakpad \ --disable-default-apps \ --disable-sync \ diff --git a/d2ypp2/services/neko/chromium.conf b/d2ypp2/services/neko/chromium.conf index 36a9e7b..694ff3a 100644 --- a/d2ypp2/services/neko/chromium.conf +++ b/d2ypp2/services/neko/chromium.conf @@ -10,6 +10,16 @@ stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true +[program:tampermonkey-provisioning] +command=/bin/sh -c 'cd /var/www/provisioning && if command -v python3 >/dev/null 2>&1; then exec python3 -m http.server 18080 --bind 127.0.0.1; fi; if command -v busybox >/dev/null 2>&1; then exec busybox httpd -f -p 127.0.0.1:18080 -h /var/www/provisioning; fi; echo "no static file server for Tampermonkey provisioning"; sleep 3600' +autorestart=true +priority=250 +user=%(ENV_USER)s +stdout_logfile=/var/log/neko/tampermonkey-provisioning.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=3 +redirect_stderr=true + [program:openbox] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/openbox --config-file /etc/neko/openbox.xml diff --git a/d2ypp2/services/neko/google-chrome.conf b/d2ypp2/services/neko/google-chrome.conf index f47926e..1337b75 100644 --- a/d2ypp2/services/neko/google-chrome.conf +++ b/d2ypp2/services/neko/google-chrome.conf @@ -10,6 +10,16 @@ stdout_logfile_maxbytes=100MB stdout_logfile_backups=10 redirect_stderr=true +[program:tampermonkey-provisioning] +command=/bin/sh -c 'cd /var/www/provisioning && if command -v python3 >/dev/null 2>&1; then exec python3 -m http.server 18080 --bind 127.0.0.1; fi; if command -v busybox >/dev/null 2>&1; then exec busybox httpd -f -p 127.0.0.1:18080 -h /var/www/provisioning; fi; echo "no static file server for Tampermonkey provisioning"; sleep 3600' +autorestart=true +priority=250 +user=%(ENV_USER)s +stdout_logfile=/var/log/neko/tampermonkey-provisioning.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=3 +redirect_stderr=true + [program:openbox] environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" command=/usr/bin/openbox --config-file /etc/neko/openbox.xml diff --git a/d2ypp2/services/neko/policies/policies.json b/d2ypp2/services/neko/policies/policies.json index 3ada497..a94ceb6 100644 --- a/d2ypp2/services/neko/policies/policies.json +++ b/d2ypp2/services/neko/policies/policies.json @@ -2,7 +2,6 @@ "ExtensionInstallForcelist": [ "dhdgffkkebhmkfjojejmpbldmpobfkfo;https://clients2.google.com/service/update2/crx" ], - "ExtensionInstallAllowlist": ["*"], "ExtensionInstallBlocklist": [], "DownloadRestrictions": 0, "AllowFileSelectionDialogs": true, @@ -23,10 +22,10 @@ "dhdgffkkebhmkfjojejmpbldmpobfkfo": { "jsonImport": [ { - "hash": "1:1b41fa75bfe51a730bb246022f619811bc508da3176463c86009fe5a0a9f16b5", - "url": "http://127.0.0.1:8080/provisioning/tampermonkey-provisioning.json", + "hash": "1:443afb465db99aef4199c33991f1350364441e496c8a27f10a82a299db87bb02", + "url": "http://127.0.0.1:18080/tampermonkey-provisioning.json", "haltOnError": false, - "installAsSystemScripts": true + "installAsSystemScripts": false } ] } diff --git a/d2ypp2/services/neko/provisioning/tampermonkey-provisioning.json b/d2ypp2/services/neko/provisioning/tampermonkey-provisioning.json index 36da46e..fe43aed 100644 --- a/d2ypp2/services/neko/provisioning/tampermonkey-provisioning.json +++ b/d2ypp2/services/neko/provisioning/tampermonkey-provisioning.json @@ -4,9 +4,10 @@ { "name": "YouTube Studio Auto Dismiss", "file_url": "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js", - "source": "// ==UserScript==\n// @name YouTube Studio Auto Dismiss\n// @namespace http://tampermonkey.net/\n// @version 1.1\n// @description 每5秒自动检测并点击 YouTube Studio 直播页面的 Dismiss 按钮,支持延迟加载\n// @author You\n// @match https://studio.youtube.com/channel/*/livestreaming\n// @match https://studio.youtube.com/video/*/livestreaming\n// @match https://studio.youtube.com/*/livestreaming\n// @license MIT\n// @grant none\n// @downloadURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.user.js\n// @updateURL https://update.greasyfork.org/scripts/557378/YouTube%20Studio%20Auto%20Dismiss.meta.js\n// ==/UserScript==\n\n(function () {\n \"use strict\";\n\n function triggerClick(el) {\n if (!el) return;\n const evt = new MouseEvent(\"click\", {\n view: window,\n bubbles: true,\n cancelable: true,\n });\n el.dispatchEvent(evt);\n console.log(\"Dismiss 按钮已点击:\", el, \"父元素:\", el.parentElement);\n }\n\n function findDismiss(root = document) {\n const allElements = root.querySelectorAll(\"button\");\n for (const el of allElements) {\n try {\n const text = el.innerText ? el.innerText.trim() : \"\";\n if (text === \"Dismiss\" && isElementVisible(el)) {\n console.log(\"找到 Dismiss 按钮:\", el);\n return el;\n }\n\n if (el.shadowRoot) {\n const shadowBtn = findDismiss(el.shadowRoot);\n if (shadowBtn) return shadowBtn;\n }\n } catch (error) {\n console.log(\"检查元素时出错:\", error);\n }\n }\n return null;\n }\n\n function isElementVisible(el) {\n const style = window.getComputedStyle(el);\n return style.display !== \"none\" && style.visibility !== \"hidden\" && el.offsetParent !== null;\n }\n\n function checkAndClick() {\n const btn = findDismiss();\n if (btn) {\n triggerClick(btn);\n } else {\n console.log(\"未找到 Dismiss 按钮或按钮不可见\");\n }\n }\n\n if (document.readyState === \"complete\") {\n checkAndClick();\n } else {\n window.addEventListener(\"load\", checkAndClick, { once: true });\n }\n\n setInterval(checkAndClick, 5000);\n})();\n", + "source": "Ly8gPT1Vc2VyU2NyaXB0PT0KLy8gQG5hbWUgICAgICAgICBZb3VUdWJlIFN0dWRpbyBBdXRvIERpc21pc3MKLy8gQG5hbWVzcGFjZSAgICBodHRwOi8vdGFtcGVybW9ua2V5Lm5ldC8KLy8gQHZlcnNpb24gICAgICAxLjEKLy8gQGRlc2NyaXB0aW9uICDmr48156eS6Ieq5Yqo5qOA5rWL5bm254K55Ye7IFlvdVR1YmUgU3R1ZGlvIOebtOaSremhtemdoueahCBEaXNtaXNzIOaMiemSru+8jOaUr+aMgeW7tui/n+WKoOi9vQovLyBAYXV0aG9yICAgICAgIFlvdQovLyBAbWF0Y2ggICAgICAgIGh0dHBzOi8vc3R1ZGlvLnlvdXR1YmUuY29tL2NoYW5uZWwvKi9saXZlc3RyZWFtaW5nCi8vIEBtYXRjaCAgICAgICAgaHR0cHM6Ly9zdHVkaW8ueW91dHViZS5jb20vdmlkZW8vKi9saXZlc3RyZWFtaW5nCi8vIEBtYXRjaCAgICAgICAgaHR0cHM6Ly9zdHVkaW8ueW91dHViZS5jb20vKi9saXZlc3RyZWFtaW5nCi8vIEBsaWNlbnNlIE1JVAovLyBAZ3JhbnQgICAgICAgIG5vbmUKLy8gQGRvd25sb2FkVVJMICBodHRwczovL3VwZGF0ZS5ncmVhc3lmb3JrLm9yZy9zY3JpcHRzLzU1NzM3OC9Zb3VUdWJlJTIwU3R1ZGlvJTIwQXV0byUyMERpc21pc3MudXNlci5qcwovLyBAdXBkYXRlVVJMICAgIGh0dHBzOi8vdXBkYXRlLmdyZWFzeWZvcmsub3JnL3NjcmlwdHMvNTU3Mzc4L1lvdVR1YmUlMjBTdHVkaW8lMjBBdXRvJTIwRGlzbWlzcy5tZXRhLmpzCi8vID09L1VzZXJTY3JpcHQ9PQoKKGZ1bmN0aW9uICgpIHsKICAidXNlIHN0cmljdCI7CgogIGZ1bmN0aW9uIHRyaWdnZXJDbGljayhlbCkgewogICAgaWYgKCFlbCkgcmV0dXJuOwogICAgY29uc3QgZXZ0ID0gbmV3IE1vdXNlRXZlbnQoImNsaWNrIiwgewogICAgICB2aWV3OiB3aW5kb3csCiAgICAgIGJ1YmJsZXM6IHRydWUsCiAgICAgIGNhbmNlbGFibGU6IHRydWUsCiAgICB9KTsKICAgIGVsLmRpc3BhdGNoRXZlbnQoZXZ0KTsKICAgIGNvbnNvbGUubG9nKCJEaXNtaXNzIOaMiemSruW3sueCueWHuzoiLCBlbCwgIueItuWFg+e0oDoiLCBlbC5wYXJlbnRFbGVtZW50KTsKICB9CgogIGZ1bmN0aW9uIGZpbmREaXNtaXNzKHJvb3QgPSBkb2N1bWVudCkgewogICAgY29uc3QgYWxsRWxlbWVudHMgPSByb290LnF1ZXJ5U2VsZWN0b3JBbGwoImJ1dHRvbiIpOwogICAgZm9yIChjb25zdCBlbCBvZiBhbGxFbGVtZW50cykgewogICAgICB0cnkgewogICAgICAgIGNvbnN0IHRleHQgPSBlbC5pbm5lclRleHQgPyBlbC5pbm5lclRleHQudHJpbSgpIDogIiI7CiAgICAgICAgaWYgKHRleHQgPT09ICJEaXNtaXNzIiAmJiBpc0VsZW1lbnRWaXNpYmxlKGVsKSkgewogICAgICAgICAgY29uc29sZS5sb2coIuaJvuWIsCBEaXNtaXNzIOaMiemSrjoiLCBlbCk7CiAgICAgICAgICByZXR1cm4gZWw7CiAgICAgICAgfQoKICAgICAgICBpZiAoZWwuc2hhZG93Um9vdCkgewogICAgICAgICAgY29uc3Qgc2hhZG93QnRuID0gZmluZERpc21pc3MoZWwuc2hhZG93Um9vdCk7CiAgICAgICAgICBpZiAoc2hhZG93QnRuKSByZXR1cm4gc2hhZG93QnRuOwogICAgICAgIH0KICAgICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgICBjb25zb2xlLmxvZygi5qOA5p+l5YWD57Sg5pe25Ye66ZSZOiIsIGVycm9yKTsKICAgICAgfQogICAgfQogICAgcmV0dXJuIG51bGw7CiAgfQoKICBmdW5jdGlvbiBpc0VsZW1lbnRWaXNpYmxlKGVsKSB7CiAgICBjb25zdCBzdHlsZSA9IHdpbmRvdy5nZXRDb21wdXRlZFN0eWxlKGVsKTsKICAgIHJldHVybiBzdHlsZS5kaXNwbGF5ICE9PSAibm9uZSIgJiYgc3R5bGUudmlzaWJpbGl0eSAhPT0gImhpZGRlbiIgJiYgZWwub2Zmc2V0UGFyZW50ICE9PSBudWxsOwogIH0KCiAgZnVuY3Rpb24gY2hlY2tBbmRDbGljaygpIHsKICAgIGNvbnN0IGJ0biA9IGZpbmREaXNtaXNzKCk7CiAgICBpZiAoYnRuKSB7CiAgICAgIHRyaWdnZXJDbGljayhidG4pOwogICAgfSBlbHNlIHsKICAgICAgY29uc29sZS5sb2coIuacquaJvuWIsCBEaXNtaXNzIOaMiemSruaIluaMiemSruS4jeWPr+ingSIpOwogICAgfQogIH0KCiAgaWYgKGRvY3VtZW50LnJlYWR5U3RhdGUgPT09ICJjb21wbGV0ZSIpIHsKICAgIGNoZWNrQW5kQ2xpY2soKTsKICB9IGVsc2UgewogICAgd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoImxvYWQiLCBjaGVja0FuZENsaWNrLCB7IG9uY2U6IHRydWUgfSk7CiAgfQoKICBzZXRJbnRlcnZhbChjaGVja0FuZENsaWNrLCA1MDAwKTsKfSkoKTsK", "enabled": true, - "position": 1 + "position": 1, + "options": {} } ], "settings": { diff --git a/d2ypp2/src/android_control.py b/d2ypp2/src/android_control.py index 4579a4c..6e968f2 100644 --- a/d2ypp2/src/android_control.py +++ b/d2ypp2/src/android_control.py @@ -525,13 +525,21 @@ async def android_logcat_recent(serial: str, lines: int = 200) -> str: def _strip_to_png(data: bytes) -> bytes: if not data: return data - stripped = data.lstrip().replace(b"\r\n", b"\n") sig = b"\x89PNG\r\n\x1a\n" + # Some adb paths may prepend shell noise. Do not normalize CRLF inside the + # PNG body: replacing bytes globally corrupts the PNG signature and chunks. + stripped = data.lstrip(b"\x00\t\r\n ") idx = stripped.find(sig) - if idx > 0: - stripped = stripped[idx:] - if stripped.startswith(b"\n\x89PNG"): - stripped = stripped[1:] + if idx >= 0: + return stripped[idx:] + + # Defensive repair for already line-ending-normalized signatures. Only the + # 8-byte signature is repaired; the compressed image body is left untouched. + alt_sig = b"\x89PNG\n\x1a\n" + idx_alt = stripped.find(alt_sig) + if idx_alt >= 0: + candidate = stripped[idx_alt:] + return sig + candidate[len(alt_sig):] return stripped @@ -565,8 +573,9 @@ async def android_screenshot(serial: str, *, exec_timeout: float = 14.0) -> byte last_err = pull_err or "adb pull failed" continue pulled = tmp.read_bytes() - if _is_png(pulled) or pulled.startswith(b"\x89PNG"): - return pulled.replace(b"\r\n", b"\n") + normalized_pulled = _strip_to_png(pulled) + if _is_png(normalized_pulled): + return normalized_pulled last_err = "Pulled file is not a valid PNG" finally: tmp.unlink(missing_ok=True) diff --git a/d2ypp2/src/control_plane.py b/d2ypp2/src/control_plane.py index 81d957a..bd79a5c 100644 --- a/d2ypp2/src/control_plane.py +++ b/d2ypp2/src/control_plane.py @@ -29,6 +29,7 @@ BASH_BIN = "/bin/bash" if Path("/bin/bash").is_file() else (shutil.which("bash") _PRO_URL_CONFIG_INI = re.compile(r"^URL_config\.[a-zA-Z0-9_.-]+\.ini$") _PRO_YOUTUBE_INI = re.compile(r"^youtube\.[a-zA-Z0-9_.-]+\.ini$") +_NETWORK_RATE_CACHE: dict[str, Any] = {"timestamp": 0.0, "interfaces": {}} @dataclass(frozen=True) @@ -497,12 +498,10 @@ def _env_int(env: dict[str, str], key: str, default: int) -> int: def _neko_http_ports(env: dict[str, str]) -> list[int]: - count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2))) - return [ - _env_int(env, "NEKO_HTTP_PORT_1", _env_int(env, "NEKO_HTTP_PORT", 9200)), - _env_int(env, "NEKO_HTTP_PORT_2", 9201), - _env_int(env, "NEKO_HTTP_PORT_3", 9202), - ][:count] + max_count = max(2, min(64, _env_int(env, "NEKO_MAX_INSTANCE_COUNT", 64))) + count = max(2, min(max_count, _env_int(env, "NEKO_INSTANCE_COUNT", 2))) + base = _env_int(env, "NEKO_HTTP_PORT", 9200) + return [_env_int(env, f"NEKO_HTTP_PORT_{idx}", base + idx - 1) for idx in range(1, count + 1)] def _neko_instances(mdns_host: str, env: dict[str, str]) -> list[dict[str, Any]]: @@ -790,16 +789,24 @@ def _iface_operstate(name: str) -> str: def network_iface_stats() -> dict[str, Any]: - """Linux /proc/net/dev cumulative counters plus current link bandwidth hints.""" + """Linux /proc/net/dev counters with per-refresh bandwidth rates.""" dev = Path("/proc/net/dev") interfaces: list[dict[str, Any]] = [] if not dev.is_file(): - return {"interfaces": interfaces} + return {"interfaces": interfaces, "total_rx_bytes": 0, "total_tx_bytes": 0, "total_rx_bps": 0.0, "total_tx_bps": 0.0} skip_prefixes = ("lo", "docker", "veth", "br-", "virbr", "tun", "tap") try: lines = dev.read_text(encoding="utf-8", errors="replace").splitlines() except OSError: - return {"interfaces": interfaces} + return {"interfaces": interfaces, "total_rx_bytes": 0, "total_tx_bytes": 0, "total_rx_bps": 0.0, "total_tx_bps": 0.0} + now = time.monotonic() + previous_ts = float(_NETWORK_RATE_CACHE.get("timestamp") or 0.0) + previous = _NETWORK_RATE_CACHE.get("interfaces") or {} + next_cache: dict[str, dict[str, int]] = {} + total_rx = 0 + total_tx = 0 + total_rx_bps = 0.0 + total_tx_bps = 0.0 for line in lines[2:]: if ":" not in line: continue @@ -811,20 +818,50 @@ def network_iface_stats() -> dict[str, Any]: if len(parts) < 16: continue try: + rx_bytes = int(parts[0]) + rx_packets = int(parts[1]) + tx_bytes = int(parts[8]) + tx_packets = int(parts[9]) + total_rx += rx_bytes + total_tx += tx_bytes + prev = previous.get(name) if isinstance(previous, dict) else None + elapsed = max(0.0, now - previous_ts) if previous_ts else 0.0 + rx_bps = 0.0 + tx_bps = 0.0 + if isinstance(prev, dict) and elapsed > 0: + prev_rx = int(prev.get("rx_bytes", rx_bytes)) + prev_tx = int(prev.get("tx_bytes", tx_bytes)) + rx_bps = max(0.0, (rx_bytes - prev_rx) / elapsed) + tx_bps = max(0.0, (tx_bytes - prev_tx) / elapsed) + total_rx_bps += rx_bps + total_tx_bps += tx_bps + next_cache[name] = {"rx_bytes": rx_bytes, "tx_bytes": tx_bytes} interfaces.append( { "name": name, - "rx_bytes": int(parts[0]), - "rx_packets": int(parts[1]), - "tx_bytes": int(parts[8]), - "tx_packets": int(parts[9]), + "rx_bytes": rx_bytes, + "rx_packets": rx_packets, + "tx_bytes": tx_bytes, + "tx_packets": tx_packets, + "rx_bps": round(rx_bps, 2), + "tx_bps": round(tx_bps, 2), + "sample_interval_seconds": round(elapsed, 3), "speed_mbps": _iface_speed_mbps(name), "operstate": _iface_operstate(name), } ) except (ValueError, IndexError): continue - return {"interfaces": interfaces} + _NETWORK_RATE_CACHE["timestamp"] = now + _NETWORK_RATE_CACHE["interfaces"] = next_cache + return { + "interfaces": interfaces, + "total_rx_bytes": total_rx, + "total_tx_bytes": total_tx, + "total_rx_bps": round(total_rx_bps, 2), + "total_tx_bps": round(total_tx_bps, 2), + "sample_interval_seconds": round(max(0.0, now - previous_ts) if previous_ts else 0.0, 3), + } def recent_pm2_log_lines(max_lines: int = 14) -> list[str]: @@ -878,7 +915,7 @@ async def stack_stream_probes() -> dict[str, Any]: http = _probe_http_get(f"http://127.0.0.1:{port}/", timeout=2.5) instances.append({"id": idx, "port": port, "tcp": tcp, "http": http}) primary = instances[0] if instances else {"port": 0, "tcp": {}, "http": {}} - required_count = max(1, min(3, _env_int(env, "NEKO_INSTANCE_COUNT", 2))) + required_count = len(neko_ports) return { "port": primary["port"], "tcp": primary["tcp"], diff --git a/d2ypp2/src/hub_routes.py b/d2ypp2/src/hub_routes.py index a74d8ed..5fa36f6 100644 --- a/d2ypp2/src/hub_routes.py +++ b/d2ypp2/src/hub_routes.py @@ -26,6 +26,7 @@ from src.web_process_backend import WebProcessBackend from src.youtube_ini_sync import write_youtube_ini_stream_key from src.redroid_control import redroid_runtime_summary from src.hdmi_control import hdmi_capture_summary +from src.neko_capacity import ensure_neko_capacity_for_youtube_channels from src.youtube_pro_runtime import normalize_youtube_pro_channels router = APIRouter(prefix="/hub", tags=["无人直播系统"]) @@ -268,14 +269,15 @@ async def hub_youtube_pro_channels_read() -> dict[str, Any]: @router.post("/youtube_pro_channels") -async def hub_youtube_pro_channels_write(body: YoutubeProChannelsBody) -> dict[str, str]: +async def hub_youtube_pro_channels_write(body: YoutubeProChannelsBody) -> dict[str, Any]: from src.hub_kv_store import set_youtube_pro_channels try: set_youtube_pro_channels(body.channels) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc - return {"message": "ok"} + capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, _repo_root, body.channels) + return {"message": "ok", "neko": capacity} @router.get("/services/declared") diff --git a/d2ypp2/src/neko_capacity.py b/d2ypp2/src/neko_capacity.py new file mode 100644 index 0000000..2f155b4 --- /dev/null +++ b/d2ypp2/src/neko_capacity.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from typing import Any, Iterable + +from src.youtube_pro_runtime import normalize_youtube_pro_channels + +MIN_NEKO_INSTANCES = 2 +MAX_MANAGED_NEKO_INSTANCES = 64 + + +def desired_neko_count_for_youtube_channels(channels: Iterable[Any]) -> int: + normalized = normalize_youtube_pro_channels(channels) + return max(MIN_NEKO_INSTANCES, min(MAX_MANAGED_NEKO_INSTANCES, len(normalized) or MIN_NEKO_INSTANCES)) + + +def _env_int(value: str | None) -> int | None: + if value is None: + return None + try: + return int(value.strip()) + except (TypeError, ValueError): + return None + + +def _read_env_value(path: Path, key: str) -> str | None: + if not path.is_file(): + return None + try: + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + except OSError: + return None + prefix = f"{key}=" + for raw in lines: + line = raw.strip() + if line.startswith(prefix): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return None + + +def upsert_env_value(path: Path, key: str, value: str) -> bool: + path.parent.mkdir(parents=True, exist_ok=True) + lines: list[str] + if path.is_file(): + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + else: + lines = [] + prefix = f"{key}=" + next_line = f"{key}={value}" + changed = False + for idx, raw in enumerate(lines): + if raw.strip().startswith(prefix): + if raw != next_line: + lines[idx] = next_line + changed = True + break + else: + if lines and lines[-1].strip(): + lines.append("") + lines.append(next_line) + changed = True + if changed: + path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + return changed + + +def ensure_neko_capacity_for_youtube_channels( + repo_root: Path | None, + channels: Iterable[Any], + *, + restart: bool = True, +) -> dict[str, Any]: + desired = desired_neko_count_for_youtube_channels(channels) + if repo_root is None: + return {"desired": desired, "changed": False, "restarted": False, "reason": "repo_root_missing"} + root = Path(repo_root) + env_path = root / "config" / "system-stack.env" + current_raw = _read_env_value(env_path, "NEKO_INSTANCE_COUNT") + current = _env_int(current_raw) + if current is not None and current >= desired: + return {"desired": desired, "current": current, "changed": False, "restarted": False} + + changed = upsert_env_value(env_path, "NEKO_INSTANCE_COUNT", str(desired)) + restarted = False + reason = "" + service_ctl = root / "scripts" / "linux" / "service_ctl.sh" + if restart and service_ctl.is_file(): + log_dir = root / "logs" + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / "neko-capacity.log" + try: + with log_file.open("ab") as stream: + subprocess.Popen( + ["bash", str(service_ctl), "neko", "restart"], + cwd=str(root), + stdout=stream, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + restarted = True + except OSError as exc: + reason = f"restart_spawn_failed: {exc}" + elif restart: + reason = "service_ctl_missing" + return { + "desired": desired, + "current": current_raw, + "changed": changed, + "restarted": restarted, + "reason": reason, + } diff --git a/d2ypp2/tests/test_android_redroid.py b/d2ypp2/tests/test_android_redroid.py index b471582..c1ebaa6 100644 --- a/d2ypp2/tests/test_android_redroid.py +++ b/d2ypp2/tests/test_android_redroid.py @@ -3,7 +3,7 @@ from __future__ import annotations import unittest from unittest.mock import patch -from src.android_control import enrich_device_row +from src.android_control import _is_png, _strip_to_png, enrich_device_row from src.redroid_control import load_redroid_env, redroid_instance_name, redroid_instance_port @@ -25,6 +25,17 @@ class AndroidRedroidTests(unittest.TestCase): self.assertEqual(row["transport"], "tcp") self.assertEqual(row["kind"], "redroid") + def test_strip_to_png_preserves_valid_png_signature(self) -> None: + png = b"\x89PNG\r\n\x1a\n" + b"\x00\x00\x00\rIHDR" + b"\r\ninside-body" + self.assertEqual(_strip_to_png(png), png) + self.assertTrue(_is_png(_strip_to_png(png))) + + def test_strip_to_png_repairs_only_normalized_signature(self) -> None: + corrupted_sig = b"\n\n\x89PNG\n\x1a\n" + b"\x00\x00\x00\rIHDR" + repaired = _strip_to_png(corrupted_sig) + self.assertTrue(repaired.startswith(b"\x89PNG\r\n\x1a\n")) + self.assertTrue(_is_png(repaired)) + @patch("src.redroid_control.platform.machine", return_value="x86_64") def test_redroid_env_prefers_x86_alt_image(self, _machine) -> None: env = load_redroid_env() diff --git a/d2ypp2/tests/test_control_plane_services.py b/d2ypp2/tests/test_control_plane_services.py index 09af377..7a23040 100644 --- a/d2ypp2/tests/test_control_plane_services.py +++ b/d2ypp2/tests/test_control_plane_services.py @@ -5,7 +5,7 @@ import tempfile from pathlib import Path from unittest.mock import patch -from src.control_plane import SERVICE_SPECS, deployment_report, stack_summary +from src.control_plane import SERVICE_SPECS, deployment_report, network_iface_stats, stack_summary class ControlPlaneServiceTests(unittest.TestCase): @@ -23,7 +23,8 @@ class ControlPlaneServiceTests(unittest.TestCase): ) def test_stack_summary_exposes_new_quick_links(self) -> None: - summary = stack_summary() + with patch("src.control_plane.load_stack_env", return_value={"HOSTNAME_ALIAS": "live"}): + summary = stack_summary() links = summary["quick_links"] for key in ( "mitmproxy", @@ -34,13 +35,29 @@ class ControlPlaneServiceTests(unittest.TestCase): "neko_browser", "neko_browser_1", "neko_browser_2", - "neko_browser_3", ): self.assertIn(key, links) neko_instances = summary["neko_instances"] - self.assertEqual([item["id"] for item in neko_instances], [1, 2, 3]) + self.assertEqual([item["id"] for item in neko_instances], [1, 2]) self.assertEqual(links["neko_browser"], links["neko_browser_1"]) + def test_stack_summary_exposes_pro_neko_links_when_configured(self) -> None: + with patch("src.control_plane.load_stack_env", return_value={"HOSTNAME_ALIAS": "live", "NEKO_INSTANCE_COUNT": "4"}): + summary = stack_summary() + links = summary["quick_links"] + self.assertIn("neko_browser_3", links) + self.assertIn("neko_browser_4", links) + self.assertEqual([item["id"] for item in summary["neko_instances"]], [1, 2, 3, 4]) + + def test_network_stats_expose_realtime_and_total_bandwidth_fields(self) -> None: + stats = network_iface_stats() + for key in ("interfaces", "total_rx_bytes", "total_tx_bytes", "total_rx_bps", "total_tx_bps"): + self.assertIn(key, stats) + for iface in stats["interfaces"]: + self.assertIn("rx_bps", iface) + self.assertIn("tx_bps", iface) + self.assertIn("sample_interval_seconds", iface) + def test_deployment_report_marks_live_local_ready_when_checks_pass(self) -> None: with tempfile.TemporaryDirectory() as tmp: base_dir = Path(tmp) diff --git a/d2ypp2/tests/test_install_bootstrap.py b/d2ypp2/tests/test_install_bootstrap.py new file mode 100644 index 0000000..107a3f1 --- /dev/null +++ b/d2ypp2/tests/test_install_bootstrap.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import importlib.util +import os +import re +import stat +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +class InstallBootstrapTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.script = (ROOT / "install.sh").read_text(encoding="utf-8") + + def test_install_sh_is_remote_bootstrapper_not_thin_wrapper(self) -> None: + mode = (ROOT / "install.sh").stat().st_mode + self.assertTrue(mode & stat.S_IXUSR, "install.sh must stay executable for local installs") + self.assertIn("SHD2YPP2 remote bootstrapper", self.script) + self.assertIn("curl -fsSL /install.sh | bash", self.script) + self.assertIn("DEFAULT_INSTALL_DIR=\"/opt/live/d2ypp\"", self.script) + self.assertIn("DEFAULT_LOG_DIR=\"/var/log/shd2ypp2\"", self.script) + self.assertIn("DEFAULT_LOCK_FILE=\"/run/lock/shd2ypp2-install.lock\"", self.script) + self.assertNotRegex(self.script, r"exec bash \"\\$ROOT_DIR/install-all\\.sh\"") + + def test_remote_source_defaults_match_project_layout(self) -> None: + self.assertIn('DEFAULT_REPO_URL="http://gitlab.nomadro.com/gitlab-instance-0a899031/sh.git"', self.script) + self.assertIn('DEFAULT_REPO_REF="best"', self.script) + self.assertIn('DEFAULT_REPO_SUBDIR="d2ypp2"', self.script) + self.assertIn("LIVE_REPO_URL", self.script) + self.assertIn("LIVE_REPO_REF", self.script) + self.assertIn("LIVE_REPO_SUBDIR", self.script) + self.assertIn("git clone --depth 1 --branch", self.script) + self.assertIn("Fetched source does not contain SHD2YPP2 project", self.script) + + def test_profiles_are_normalized_for_existing_stack_installer(self) -> None: + self.assertRegex(self.script, r"all\|full\).*PROFILE=\"all\"") + self.assertRegex(self.script, r"hub\|core\|sh\).*PROFILE=\"hub\"") + self.assertRegex(self.script, r"business\|live\|app\|sh2\).*PROFILE=\"business\"") + self.assertIn('bash "$stack" "$PROFILE"', self.script) + + def test_bootstrap_has_lock_logging_and_privilege_handling(self) -> None: + self.assertIn("setup_logging()", self.script) + self.assertIn("acquire_lock()", self.script) + self.assertIn("flock -n 9", self.script) + self.assertIn("sudo -E", self.script) + self.assertIn("LIVE_INSTALL_LOG_FILE", self.script) + self.assertIn("tee -a \"$LOG_FILE\"", self.script) + + def test_sync_excludes_runtime_state_and_preserves_config(self) -> None: + required_excludes = [ + ".venv/", + "node_modules/", + "web-console/node_modules/", + "logs/", + "downloads/", + "backup_config/", + ".pm2/", + ".process_runner/", + "services/neko/data/", + "config/system-stack.env", + ] + for item in required_excludes: + self.assertIn(f"--exclude '{item}'", self.script) + self.assertIn("rsync -a --delete", self.script) + self.assertIn('LIVE_STACK_REPO_ROOT="$INSTALL_DIR"', self.script) + self.assertIn('project="$(sync_to_install_dir "$project")"', self.script) + + def test_post_install_verifies_real_control_plane(self) -> None: + self.assertIn("post_verify()", self.script) + self.assertIn("http://127.0.0.1:$port/health", self.script) + self.assertIn("http://127.0.0.1:$port/deploy/check", self.script) + self.assertIn("journalctl -u live-console.service -n 120", self.script) + self.assertIn("Control plane did not become healthy", self.script) + self.assertIn('tools/verify_local_install.py', self.script) + self.assertIn('python3 "$verifier" --install-dir "$INSTALL_DIR"', self.script) + + def test_launch_exports_cli_host_and_port_to_runtime_environment(self) -> None: + spec = importlib.util.spec_from_file_location("launch_for_test", ROOT / "scripts" / "launch.py") + self.assertIsNotNone(spec) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + sys.modules[spec.name] = module + try: + spec.loader.exec_module(module) + finally: + sys.modules.pop(spec.name, None) + + captured: dict[str, str | bool] = {} + + def fake_run_prod(host: str, port: str, skip_build: bool, reload: bool) -> None: + captured.update({"host": host, "port": port, "skip_build": skip_build, "reload": reload}) + + old_host = os.environ.pop("HOST", None) + old_port = os.environ.pop("PORT", None) + try: + with patch.object( + sys, + "argv", + ["launch.py", "--host", "127.0.0.1", "--port", "18101", "--skip-build", "--no-env-check"], + ): + with patch.object(module, "load_runtime_env"): + with patch.object(module, "raise_nofile_limit"): + with patch.object(module, "ensure_runtime_layout", return_value=[]): + with patch.object(module, "run_prod", side_effect=fake_run_prod): + module.main() + self.assertEqual(captured["host"], "127.0.0.1") + self.assertEqual(captured["port"], "18101") + self.assertEqual(os.environ["HOST"], "127.0.0.1") + self.assertEqual(os.environ["PORT"], "18101") + finally: + if old_host is not None: + os.environ["HOST"] = old_host + else: + os.environ.pop("HOST", None) + if old_port is not None: + os.environ["PORT"] = old_port + else: + os.environ.pop("PORT", None) + + def test_no_placeholder_language_in_bootstrap_contract(self) -> None: + forbidden = ["TODO", "FIXME", "theoretically", "manual only", "手动修改"] + for marker in forbidden: + self.assertNotIn(marker, self.script) + self.assertIsNone(re.search(r"\bpass\b", self.script)) + + +if __name__ == "__main__": + unittest.main() diff --git a/d2ypp2/tests/test_live_edge_watchdog.py b/d2ypp2/tests/test_live_edge_watchdog.py new file mode 100644 index 0000000..f361d52 --- /dev/null +++ b/d2ypp2/tests/test_live_edge_watchdog.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class LiveEdgeWatchdogTests(unittest.TestCase): + def test_install_stack_registers_edge_watchdog(self) -> None: + common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8") + self.assertIn("live-edge-watch.service", common) + self.assertIn("ExecStart=/bin/bash $ROOT_DIR/scripts/linux/live_edge_watch.sh", common) + self.assertIn("Restart=always", common) + self.assertIn("systemctl enable --now live-edge-watch.service", common) + + def test_watchdog_repairs_inactive_or_unhealthy_edge(self) -> None: + watch = (ROOT / "scripts" / "linux" / "live_edge_watch.sh").read_text(encoding="utf-8") + self.assertIn("LIVE_EDGE_WATCH_INTERVAL", watch) + self.assertIn("systemctl start live-console.service", watch) + self.assertIn("systemctl start live-edge.service", watch) + self.assertIn("http://127.0.0.1/health", watch) + self.assertIn("systemctl restart live-edge.service", watch) + + def test_uninstall_removes_edge_watchdog(self) -> None: + uninstall = (ROOT / "scripts" / "linux" / "uninstall_stack.sh").read_text(encoding="utf-8") + self.assertIn("live-edge-watch.service live-edge.service", uninstall) + self.assertIn("rm -f /etc/systemd/system/live-edge-watch.service", uninstall) + + def test_stack_env_exposes_watch_interval(self) -> None: + env_example = (ROOT / "config" / "system-stack.env.example").read_text(encoding="utf-8") + self.assertIn("LIVE_EDGE_WATCH_INTERVAL=15", env_example) + + +if __name__ == "__main__": + unittest.main() diff --git a/d2ypp2/tests/test_neko_capacity.py b/d2ypp2/tests/test_neko_capacity.py new file mode 100644 index 0000000..89846ea --- /dev/null +++ b/d2ypp2/tests/test_neko_capacity.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from src.neko_capacity import ( + desired_neko_count_for_youtube_channels, + ensure_neko_capacity_for_youtube_channels, + upsert_env_value, +) + + +class NekoCapacityTests(unittest.TestCase): + def test_desired_count_keeps_two_instance_floor_and_sixty_four_instance_cap(self) -> None: + self.assertEqual(desired_neko_count_for_youtube_channels([]), 2) + self.assertEqual(desired_neko_count_for_youtube_channels([{"id": "lane1"}]), 2) + self.assertEqual(desired_neko_count_for_youtube_channels([{"id": "lane1"}, {"id": "lane2"}]), 2) + self.assertEqual( + desired_neko_count_for_youtube_channels( + [{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}, {"id": "lane4"}] + ), + 4, + ) + self.assertEqual(desired_neko_count_for_youtube_channels({"id": f"lane{i}"} for i in range(80)), 64) + + def test_upsert_env_value_preserves_file_and_changes_target_key(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + env = Path(tmp) / "config" / "system-stack.env" + env.parent.mkdir() + env.write_text("HOSTNAME_ALIAS=live\nNEKO_INSTANCE_COUNT=2\nPORT=8001\n", encoding="utf-8") + + changed = upsert_env_value(env, "NEKO_INSTANCE_COUNT", "3") + + self.assertTrue(changed) + self.assertEqual(env.read_text(encoding="utf-8"), "HOSTNAME_ALIAS=live\nNEKO_INSTANCE_COUNT=3\nPORT=8001\n") + + def test_ensure_capacity_increases_env_and_restarts_neko_in_background(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + env = root / "config" / "system-stack.env" + env.parent.mkdir() + env.write_text("NEKO_INSTANCE_COUNT=2\n", encoding="utf-8") + service_ctl = root / "scripts" / "linux" / "service_ctl.sh" + service_ctl.parent.mkdir(parents=True) + service_ctl.write_text("#!/usr/bin/env bash\n", encoding="utf-8") + + with patch("src.neko_capacity.subprocess.Popen") as popen: + result = ensure_neko_capacity_for_youtube_channels( + root, + [{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}], + ) + + self.assertTrue(result["changed"]) + self.assertTrue(result["restarted"]) + self.assertIn("NEKO_INSTANCE_COUNT=3", env.read_text(encoding="utf-8")) + popen.assert_called_once() + self.assertEqual(popen.call_args.args[0][-2:], ["neko", "restart"]) + + def test_ensure_capacity_keeps_saved_env_when_restart_spawn_fails(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + env = root / "config" / "system-stack.env" + env.parent.mkdir() + env.write_text("NEKO_INSTANCE_COUNT=2\n", encoding="utf-8") + service_ctl = root / "scripts" / "linux" / "service_ctl.sh" + service_ctl.parent.mkdir(parents=True) + service_ctl.write_text("#!/usr/bin/env bash\n", encoding="utf-8") + + with patch("src.neko_capacity.subprocess.Popen", side_effect=OSError("fork failed")): + result = ensure_neko_capacity_for_youtube_channels( + root, + [{"id": "lane1"}, {"id": "lane2"}, {"id": "lane3"}], + ) + + self.assertTrue(result["changed"]) + self.assertFalse(result["restarted"]) + self.assertIn("restart_spawn_failed", result["reason"]) + self.assertIn("NEKO_INSTANCE_COUNT=3", env.read_text(encoding="utf-8")) + + def test_ensure_capacity_does_not_downscale_manual_larger_count(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + env = root / "config" / "system-stack.env" + env.parent.mkdir() + env.write_text("NEKO_INSTANCE_COUNT=3\n", encoding="utf-8") + + result = ensure_neko_capacity_for_youtube_channels(root, [{"id": "lane1"}], restart=False) + + self.assertFalse(result["changed"]) + self.assertEqual(env.read_text(encoding="utf-8"), "NEKO_INSTANCE_COUNT=3\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/d2ypp2/tests/test_neko_config.py b/d2ypp2/tests/test_neko_config.py index 891c46a..0140b0a 100644 --- a/d2ypp2/tests/test_neko_config.py +++ b/d2ypp2/tests/test_neko_config.py @@ -1,9 +1,13 @@ from __future__ import annotations +import base64 import importlib.util import json +import os +import tempfile import unittest from pathlib import Path +from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] @@ -11,14 +15,16 @@ ROOT = Path(__file__).resolve().parents[1] class NekoConfigTests(unittest.TestCase): def test_browser_launch_cleanup_script_still_prunes_profile_bloat(self) -> None: - script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8") + script_path = ROOT / "services" / "neko" / "browser-launch.sh" + self.assertTrue(os.access(script_path, os.X_OK)) + script = script_path.read_text(encoding="utf-8") self.assertIn("arch=\"$(uname -m", script) self.assertIn("--disable-software-rasterizer", script) self.assertIn("--use-gl=swiftshader", script) self.assertIn("NEKO_BROWSER_RENDER_MODE", script) self.assertIn("--start-maximized", script) self.assertIn("--disable-background-mode", script) - self.assertIn("--disable-background-networking", script) + self.assertNotIn("--disable-background-networking", script) self.assertNotIn("--disable-extensions", script) self.assertIn('ui["developer_mode"] = True', script) self.assertIn('"Local State"', script) @@ -36,16 +42,19 @@ class NekoConfigTests(unittest.TestCase): self.assertTrue(policies["VideoCaptureAllowed"]) self.assertTrue(policies["AudioCaptureAllowed"]) self.assertEqual(policies["DeveloperToolsAvailability"], 1) + self.assertNotIn("ExtensionInstallAllowlist", policies) tm_policy = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0] - self.assertEqual(tm_policy["url"], "http://127.0.0.1:8080/provisioning/tampermonkey-provisioning.json") + self.assertEqual(tm_policy["url"], "http://127.0.0.1:18080/tampermonkey-provisioning.json") self.assertTrue(tm_policy["hash"].startswith("1:")) - self.assertTrue(tm_policy["installAsSystemScripts"]) + self.assertFalse(tm_policy["installAsSystemScripts"]) def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None: conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8") self.assertIn("command=/etc/neko/browser-launch.sh google-chrome", conf) self.assertNotIn("--bwsi", conf) self.assertIn("autorestart=true", conf) + self.assertIn("[program:tampermonkey-provisioning]", conf) + self.assertIn("127.0.0.1:18080", conf) self.assertIn("[program:openbox]", conf) self.assertIn("/usr/bin/openbox --config-file /etc/neko/openbox.xml", conf) script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8") @@ -57,12 +66,14 @@ class NekoConfigTests(unittest.TestCase): self.assertIn("command=/etc/neko/browser-launch.sh chromium", conf) self.assertNotIn("--bwsi", conf) self.assertIn("autorestart=true", conf) + self.assertIn("[program:tampermonkey-provisioning]", conf) + self.assertIn("127.0.0.1:18080", conf) self.assertIn("[program:openbox]", conf) script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8") self.assertIn('profile_dir="/home/neko/.config/chromium"', script) self.assertIn('--user-data-dir="$profile_dir"', script) - def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None: + def test_legacy_neko_compose_template_mounts_only_selected_supervisor_conf(self) -> None: compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8") self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/${NEKO_BROWSER_SUPERVISOR_CONF}:ro", compose) self.assertIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose) @@ -75,6 +86,44 @@ class NekoConfigTests(unittest.TestCase): self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose) self.assertNotIn("/etc/neko/supervisord/browser.conf:ro", compose) + def test_dynamic_neko_compose_renderer_supports_more_than_three_instances(self) -> None: + spec = importlib.util.spec_from_file_location( + "render_neko_compose", + ROOT / "tools" / "render_neko_compose.py", + ) + self.assertIsNotNone(spec) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + + with tempfile.TemporaryDirectory() as tmp: + with patch.dict( + os.environ, + { + "NEKO_IMAGE": "test-image", + "NEKO_PROFILE_HOST_DIR": f"{tmp}/profile", + "NEKO_BROWSER_SUPERVISOR_CONF": "google-chrome.conf", + }, + clear=False, + ): + rendered = module.render(4) + + self.assertIn("container_name: live-neko-4", rendered) + self.assertIn('"9203:8080"', rendered) + self.assertIn('"52303:52303/tcp"', rendered) + self.assertIn('"52400-52499:52400-52499/udp"', rendered) + self.assertIn(f'"{tmp}/profile-4:/home/neko/.config/google-chrome"', rendered) + + def test_neko_runtime_scripts_use_generated_compose(self) -> None: + service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8") + common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8") + for script in (service_ctl, common): + self.assertIn("render_neko_compose_file()", script) + self.assertIn("tools/render_neko_compose.py", script) + self.assertIn("docker-compose.generated.yml", script) + self.assertIn("compose_file=\"$(render_neko_compose_file)\"", script) + self.assertIn("NEKO_MAX_INSTANCE_COUNT:-64", script) + def test_supervisor_uses_browser_launch_script(self) -> None: chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8") chromium = (ROOT / "services" / "neko" / "chromium.conf").read_text(encoding="utf-8") @@ -90,7 +139,9 @@ class NekoConfigTests(unittest.TestCase): self.assertEqual(payload["version"], "1") self.assertEqual(payload["settings"]["runtime_content_mode"], "content") self.assertEqual(payload["scripts"][0]["file_url"], "https://greasyfork.org/scripts/557378-youtube-studio-auto-dismiss/code/YouTube%20Studio%20Auto%20Dismiss.user.js") - self.assertEqual(payload["scripts"][0]["source"], userscript_path.read_text(encoding="utf-8")) + self.assertEqual(payload["scripts"][0]["options"], {}) + decoded_source = base64.b64decode(payload["scripts"][0]["source"]).decode("utf-8") + self.assertEqual(decoded_source, userscript_path.read_text(encoding="utf-8")) spec = importlib.util.spec_from_file_location( "render_tampermonkey_provisioning", @@ -102,7 +153,9 @@ class NekoConfigTests(unittest.TestCase): spec.loader.exec_module(module) policies = json.loads((ROOT / "services" / "neko" / "policies" / "policies.json").read_text(encoding="utf-8")) - expected_hash = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0]["hash"] + tm_import = policies["3rdparty"]["extensions"]["dhdgffkkebhmkfjojejmpbldmpobfkfo"]["jsonImport"][0] + self.assertFalse(tm_import["installAsSystemScripts"]) + expected_hash = tm_import["hash"] self.assertEqual(expected_hash, f"1:{module.tm_hash(payload)}") def test_neko_scripts_use_family_specific_profile_dirs(self) -> None: @@ -113,9 +166,13 @@ class NekoConfigTests(unittest.TestCase): self.assertIn("resolve_neko_profile_dir()", common) self.assertIn('${family}-profile-v2', common) self.assertIn("resolve_neko_profile_dir_for_instance()", service_ctl) + self.assertIn("seed_neko_tampermonkey_profile()", service_ctl) + self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", service_ctl) self.assertIn("NEKO_HTTP_PORT_2", service_ctl) self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", service_ctl) self.assertIn("resolve_neko_profile_dir_for_instance()", common) + self.assertIn("seed_neko_tampermonkey_profile()", common) + self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", common) self.assertIn("NEKO_HTTP_PORT_2", common) self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", common) diff --git a/d2ypp2/tests/test_verify_local_install.py b/d2ypp2/tests/test_verify_local_install.py new file mode 100644 index 0000000..7d683b7 --- /dev/null +++ b/d2ypp2/tests/test_verify_local_install.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import argparse +import importlib.util +import json +import stat +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "verify_local_install", + ROOT / "tools" / "verify_local_install.py", +) +assert SPEC and SPEC.loader +verify_local_install = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(verify_local_install) + + +class VerifyLocalInstallTests(unittest.TestCase): + def make_install_dir(self, env_text: str) -> Path: + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + install_dir = Path(tmp.name) + env_path = install_dir / "config" / "system-stack.env" + env_path.parent.mkdir(parents=True) + env_path.write_text(env_text, encoding="utf-8") + web_index = install_dir / "web-console" / "out" / "index.html" + web_index.parent.mkdir(parents=True) + web_index.write_text("ok", encoding="utf-8") + start = install_dir / "start.sh" + start.write_text("#!/usr/bin/env bash\n", encoding="utf-8") + start.chmod(start.stat().st_mode | stat.S_IXUSR) + return install_dir + + def test_build_report_covers_control_plane_neko_and_android_screenshot(self) -> None: + install_dir = self.make_install_dir( + "\n".join( + [ + "PORT=8100", + "HOSTNAME_ALIAS=live", + "ENABLE_LIVE_EDGE_PROXY=1", + "ENABLE_NEKO=1", + "NEKO_INSTANCE_COUNT=2", + "ENABLE_ANDROID_PANEL=1", + "ENABLE_REDROID=1", + "", + ] + ) + ) + + def fake_http_json(url: str, *, timeout: float = 5.0): + if url.endswith("/health"): + return True, 200, {"status": "ok"}, "", 2.0 + if url.endswith("/deploy/check"): + return True, 200, {"ready": True, "pass_count": 7, "total_count": 7}, "", 3.0 + if url.endswith("/android/devices"): + return ( + True, + 200, + { + "available": True, + "devices": [{"serial": "127.0.0.1:5555", "state": "device"}], + }, + "", + 4.0, + ) + if url.endswith("/youtube_pro_channels"): + return True, 200, {"channels": [{"id": "lane1"}, {"id": "lane2"}]}, "", 2.0 + self.fail(f"unexpected JSON URL: {url}") + + def fake_http_bytes(url: str, *, timeout: float = 5.0): + if url == "http://127.0.0.1/health": + return True, 200, b"ok", "", 2.0 + if url.startswith("http://127.0.0.1:8100/android/screenshot?"): + return True, 200, verify_local_install.PNG_SIG + b"image", "", 6.0 + self.fail(f"unexpected bytes URL: {url}") + + def fake_http_json_post(url: str, payload: dict, *, timeout: float = 5.0): + if url == "http://127.0.0.1:8100/android/action": + self.assertEqual(payload["action"], "wake") + self.assertEqual(payload["serial"], "127.0.0.1:5555") + return True, 200, {"message": "Wake + swipe + home sent"}, "", 7.0 + self.fail(f"unexpected POST URL: {url}") + + def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0): + if "supervisorctl status google-chrome" in command: + return 0, "google-chrome RUNNING pid 8", "" + if "supervisorctl status tampermonkey-provisioning" in command: + return 0, "tampermonkey-provisioning RUNNING pid 9", "" + if "tampermonkey-provisioning.json" in command: + return 0, "", "" + if "grep -R -a -l" in command: + return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", "" + self.fail(f"unexpected docker exec: {container} {command}") + + args = argparse.Namespace( + install_dir=str(install_dir), + port="", + timeout=5.0, + skip_neko=False, + skip_android=False, + ) + with patch.object(verify_local_install, "systemd_active", return_value=(True, "active")): + with patch.object(verify_local_install, "http_json", side_effect=fake_http_json): + with patch.object(verify_local_install, "http_bytes", side_effect=fake_http_bytes): + with patch.object(verify_local_install, "http_json_post", side_effect=fake_http_json_post): + with patch.object( + verify_local_install, + "docker_ps_names", + return_value=(True, ["live-neko-1", "live-neko-2"], ""), + ): + with patch.object(verify_local_install, "docker_exec", side_effect=fake_docker_exec): + report = verify_local_install.build_report(args) + + self.assertTrue(report["ready"]) + ids = {item["id"] for item in report["checks"]} + for check_id in ( + "api_health", + "deploy_check", + "edge_health", + "live_edge_watch_service", + "neko_ip_watch_service", + "youtube_pro_neko_capacity", + "neko_1_browser", + "neko_1_tm_payload", + "neko_1_tm_storage", + "android_devices", + "android_screenshot", + "android_action", + ): + self.assertIn(check_id, ids) + + def test_android_endpoint_without_devices_is_not_a_required_install_failure(self) -> None: + checks: list[dict[str, object]] = [] + with patch.object( + verify_local_install, + "http_json", + return_value=(True, 200, {"available": False, "devices": []}, "", 3.0), + ): + verify_local_install.verify_android( + checks, + {"ENABLE_ANDROID_PANEL": "1", "ENABLE_REDROID": "1"}, + port="8001", + timeout=5.0, + ) + + report = verify_local_install.summarize(checks) + self.assertTrue(report["ready"], json.dumps(report, ensure_ascii=False)) + self.assertFalse(next(item for item in checks if item["id"] == "android_device_presence")["required"]) + + def test_required_failure_returns_degraded_summary(self) -> None: + checks: list[dict[str, object]] = [] + verify_local_install.check(checks, "api_health", "FastAPI /health", False, detail="connection refused") + report = verify_local_install.summarize(checks) + self.assertFalse(report["ready"]) + self.assertEqual(report["failing_checks"][0]["id"], "api_health") + + def test_youtube_pro_capacity_fails_when_channels_outgrow_neko_count(self) -> None: + checks: list[dict[str, object]] = [] + with patch.object( + verify_local_install, + "http_json", + return_value=(True, 200, {"channels": [{"id": "a"}, {"id": "b"}, {"id": "c"}]}, "", 1.0), + ): + verify_local_install.verify_youtube_pro_capacity( + checks, + {"ENABLE_NEKO": "1", "NEKO_INSTANCE_COUNT": "2"}, + port="8001", + timeout=5.0, + ) + + report = verify_local_install.summarize(checks) + self.assertFalse(report["ready"]) + self.assertEqual(report["failing_checks"][0]["id"], "youtube_pro_neko_capacity") + + def test_neko_required_count_matches_current_three_instance_compose_cap(self) -> None: + self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "1"}), 2) + self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "2"}), 2) + self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "4"}), 4) + self.assertEqual(verify_local_install.neko_required_count({"NEKO_INSTANCE_COUNT": "99"}), 64) + + +if __name__ == "__main__": + unittest.main() diff --git a/d2ypp2/tools/render_neko_compose.py b/d2ypp2/tools/render_neko_compose.py new file mode 100644 index 0000000..b297b68 --- /dev/null +++ b/d2ypp2/tools/render_neko_compose.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Render a dynamic Neko docker-compose file from the current environment.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path + + +def env(name: str, default: str) -> str: + value = os.environ.get(name) + return value if value not in {None, ""} else default + + +def q(value: str) -> str: + return json.dumps(str(value), ensure_ascii=False) + + +def instance_value(prefix: str, idx: int, default: str) -> str: + return env(f"{prefix}_{idx}", default) + + +def default_udp_range(idx: int) -> str: + if idx == 1: + return "52000-52099" + if idx == 2: + return "52101-52199" + if idx == 3: + return "52201-52299" + start = 52400 + ((idx - 4) * 100) + return f"{start}-{start + 99}" + + +def render(count: int) -> str: + image = env("NEKO_IMAGE", "ghcr.io/m1k1o/neko/chromium:3.0.0") + shm = env("NEKO_SHM_SIZE", "512mb") + screen = env("NEKO_DESKTOP_SCREEN", "1280x720@15") + user_pass = env("NEKO_USER_PASS", "12345678") + admin_pass = env("NEKO_ADMIN_PASS", "12345678") + icelite = env("NEKO_WEBRTC_ICELITE", "0") + nat1to1 = env("NEKO_WEBRTC_NAT1TO1", "") + max_fps = env("NEKO_MAX_FPS", "15") + video_bitrate = env("NEKO_VIDEO_BITRATE", "1200") + plugins_enabled = env("NEKO_PLUGINS_ENABLED", "true") + start_url = env("NEKO_START_URL", "https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com/") + render_mode = env("NEKO_BROWSER_RENDER_MODE", "auto") + supervisor_conf = env("NEKO_BROWSER_SUPERVISOR_CONF", "chromium.conf") + base_profile = env("NEKO_PROFILE_HOST_DIR", str(Path.cwd() / "data" / "chromium-profile-v2")) + http_base = int(env("NEKO_HTTP_PORT", "9200")) + mux_base = int(env("NEKO_WEBRTC_TCPMUX", "52300")) + + lines = [ + "# Generated by tools/render_neko_compose.py - do not hand-edit", + "services:", + ] + for idx in range(1, count + 1): + http_port = instance_value("NEKO_HTTP_PORT", idx, str(http_base + idx - 1)) + tcp_mux = instance_value("NEKO_WEBRTC_TCPMUX", idx, str(mux_base + idx - 1)) + udp_mux = instance_value("NEKO_WEBRTC_UDPMUX", idx, str(mux_base + idx - 1)) + udp_range = instance_value("NEKO_WEBRTC_UDP_RANGE", idx, default_udp_range(idx)) + epr = instance_value("NEKO_WEBRTC_EPR", idx, udp_range) + profile = instance_value("NEKO_PROFILE_HOST_DIR", idx, base_profile if idx == 1 else f"{base_profile}-{idx}") + lines.extend( + [ + f" neko{idx}:", + f" image: {q(image)}", + " restart: unless-stopped", + f" shm_size: {q(shm)}", + " cap_add:", + " - SYS_ADMIN", + f" container_name: live-neko-{idx}", + " environment:", + ' DISPLAY: ":99.0"', + f" NEKO_DESKTOP_SCREEN: {q(screen)}", + ' NEKO_SERVER_BIND: ":8080"', + ' NEKO_MEMBER_PROVIDER: "multiuser"', + f" NEKO_MEMBER_MULTIUSER_USER_PASSWORD: {q(user_pass)}", + f" NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD: {q(admin_pass)}", + f" NEKO_WEBRTC_ICELITE: {q(icelite)}", + f" NEKO_WEBRTC_NAT1TO1: {q(nat1to1)}", + f" NEKO_MAX_FPS: {q(max_fps)}", + f" NEKO_VIDEO_BITRATE: {q(video_bitrate)}", + f" NEKO_PLUGINS_ENABLED: {q(plugins_enabled)}", + ' NEKO_PLUGINS_DIR: "/etc/neko/plugins/"', + f" NEKO_START_URL: {q(start_url)}", + f" NEKO_BROWSER_RENDER_MODE: {q(render_mode)}", + f" NEKO_WEBRTC_EPR: {q(epr)}", + f" NEKO_WEBRTC_TCPMUX: {q(tcp_mux)}", + f" NEKO_WEBRTC_UDPMUX: {q(udp_mux)}", + " ports:", + f" - {q(f'{http_port}:8080')}", + f" - {q(f'{tcp_mux}:{tcp_mux}/tcp')}", + f" - {q(f'{udp_mux}:{udp_mux}/udp')}", + f" - {q(f'{udp_range}:{udp_range}/udp')}", + " volumes:", + f" - {q(f'{profile}:/home/neko/.config/chromium')}", + f" - {q(f'{profile}:/home/neko/.config/google-chrome')}", + ' - "./policies:/etc/chromium/policies/managed:ro"', + ' - "./policies:/etc/opt/chrome/policies/managed:ro"', + ' - "./provisioning:/var/www/provisioning:ro"', + ' - "./browser-launch.sh:/etc/neko/browser-launch.sh:ro"', + f" - {q(f'./{supervisor_conf}:/etc/neko/supervisord/{supervisor_conf}:ro')}", + ] + ) + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Render Neko docker compose") + parser.add_argument("--count", type=int, required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args() + count = max(2, min(64, args.count)) + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(render(count), encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/d2ypp2/tools/render_tampermonkey_provisioning.py b/d2ypp2/tools/render_tampermonkey_provisioning.py index 5c68a8f..9a07eb8 100644 --- a/d2ypp2/tools/render_tampermonkey_provisioning.py +++ b/d2ypp2/tools/render_tampermonkey_provisioning.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import base64 import hashlib import json from pathlib import Path @@ -53,15 +54,17 @@ def tm_hash(value: Any, seen: list[int] | None = None) -> str: def build_payload(script_text: str) -> dict[str, Any]: + encoded_source = base64.b64encode(script_text.encode("utf-8")).decode("ascii") return { "version": "1", "scripts": [ { "name": "YouTube Studio Auto Dismiss", "file_url": GREASYFORK_URL, - "source": script_text, + "source": encoded_source, "enabled": True, "position": 1, + "options": {}, } ], "settings": { diff --git a/d2ypp2/tools/verify_local_install.py b/d2ypp2/tools/verify_local_install.py new file mode 100755 index 0000000..37af6f6 --- /dev/null +++ b/d2ypp2/tools/verify_local_install.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +"""Local production verifier for SHD2YPP2 installs. + +This is intentionally stdlib-only so it can run immediately after install.sh on +fresh Debian/Ubuntu/Armbian hosts. It checks the real delivery path, not only a +single process: files, systemd, API, edge proxy, Docker/Neko, Tampermonkey +provisioning, and Android screenshot/control readiness when devices exist. +""" +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + + +DEFAULT_INSTALL_DIR = Path("/opt/live/d2ypp") +PNG_SIG = b"\x89PNG\r\n\x1a\n" + + +def run(cmd: list[str], *, timeout: float = 8.0) -> tuple[int, str, str]: + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + errors="replace", + ) + except FileNotFoundError as exc: + return 127, "", str(exc) + except subprocess.TimeoutExpired as exc: + out = exc.stdout if isinstance(exc.stdout, str) else "" + err = exc.stderr if isinstance(exc.stderr, str) else "" + return 124, out, err or f"timeout after {timeout}s" + return proc.returncode, proc.stdout or "", proc.stderr or "" + + +def read_env(path: Path) -> dict[str, str]: + env: dict[str, str] = {} + if not path.is_file(): + return env + for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + if key: + env[key] = value + return env + + +def env_flag(env: dict[str, str], key: str, default: bool = True) -> bool: + raw = env.get(key) + if raw is None or raw == "": + return default + return raw.strip().lower() not in {"0", "false", "no", "off", "disable", "disabled"} + + +def env_int(env: dict[str, str], key: str, default: int) -> int: + try: + return int(str(env.get(key, default)).strip()) + except (TypeError, ValueError): + return default + + +def check( + checks: list[dict[str, Any]], + check_id: str, + label: str, + ok: bool, + *, + required: bool = True, + detail: str = "", + hint: str = "", + data: dict[str, Any] | None = None, +) -> dict[str, Any]: + item = { + "id": check_id, + "label": label, + "ok": bool(ok), + "required": bool(required), + "detail": detail, + } + if hint: + item["hint"] = hint + if data: + item.update(data) + checks.append(item) + return item + + +def http_bytes(url: str, *, timeout: float = 5.0) -> tuple[bool, int, bytes, str, float]: + start = time.monotonic() + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + body = resp.read() + return True, int(resp.status), body, "", round((time.monotonic() - start) * 1000, 1) + except urllib.error.HTTPError as exc: + body = exc.read() if hasattr(exc, "read") else b"" + return False, int(exc.code), body, str(exc), round((time.monotonic() - start) * 1000, 1) + except (OSError, urllib.error.URLError) as exc: + return False, 0, b"", str(exc), round((time.monotonic() - start) * 1000, 1) + + +def http_json(url: str, *, timeout: float = 5.0) -> tuple[bool, int, dict[str, Any], str, float]: + ok, code, body, err, latency = http_bytes(url, timeout=timeout) + if not body: + return ok, code, {}, err, latency + try: + return ok and 200 <= code < 300, code, json.loads(body.decode("utf-8", errors="replace")), err, latency + except json.JSONDecodeError as exc: + return False, code, {}, f"invalid JSON: {exc}", latency + + +def http_json_post(url: str, payload: dict[str, Any], *, timeout: float = 5.0) -> tuple[bool, int, dict[str, Any], str, float]: + start = time.monotonic() + body = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + url, + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as resp: + raw = resp.read() + latency = round((time.monotonic() - start) * 1000, 1) + data = json.loads(raw.decode("utf-8", errors="replace")) if raw else {} + return True, int(resp.status), data, "", latency + except urllib.error.HTTPError as exc: + raw = exc.read() if hasattr(exc, "read") else b"" + latency = round((time.monotonic() - start) * 1000, 1) + try: + data = json.loads(raw.decode("utf-8", errors="replace")) if raw else {} + except json.JSONDecodeError: + data = {} + return False, int(exc.code), data, str(exc), latency + except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc: + return False, 0, {}, str(exc), round((time.monotonic() - start) * 1000, 1) + + +def systemd_active(unit: str) -> tuple[bool, str]: + code, out, err = run(["systemctl", "is-active", unit], timeout=5) + state = (out or err).strip() + return code == 0 and state == "active", state or f"exit={code}" + + +def docker_ps_names() -> tuple[bool, list[str], str]: + if not shutil.which("docker"): + return False, [], "docker missing" + code, out, err = run(["docker", "ps", "--format", "{{.Names}}"], timeout=8) + if code != 0: + return False, [], err.strip() or out.strip() or f"exit={code}" + return True, [line.strip() for line in out.splitlines() if line.strip()], "" + + +def docker_exec(container: str, command: str, *, timeout: float = 8.0) -> tuple[int, str, str]: + return run(["docker", "exec", container, "sh", "-lc", command], timeout=timeout) + + +def verify_files(checks: list[dict[str, Any]], install_dir: Path) -> None: + check( + checks, + "install_dir", + "Runtime install directory", + install_dir.is_dir(), + detail=str(install_dir), + hint="Run install.sh again; it syncs the project to /opt/live/d2ypp by default.", + ) + check( + checks, + "stack_env", + "system-stack.env", + (install_dir / "config" / "system-stack.env").is_file(), + detail=str(install_dir / "config" / "system-stack.env"), + hint="Copy config/system-stack.env.example or rerun the installer.", + ) + check( + checks, + "web_assets", + "Exported web-console assets", + (install_dir / "web-console" / "out" / "index.html").is_file(), + detail=str(install_dir / "web-console" / "out" / "index.html"), + hint="Run sudo bash upgrade-live.sh or rebuild web-console.", + ) + check( + checks, + "start_script", + "start.sh executable", + os.access(install_dir / "start.sh", os.X_OK), + detail=str(install_dir / "start.sh"), + hint="Run chmod +x start.sh and refresh live-console.service.", + ) + + +def verify_systemd(checks: list[dict[str, Any]], env: dict[str, str]) -> None: + ok, detail = systemd_active("live-console.service") + check( + checks, + "live_console_service", + "live-console.service", + ok, + detail=detail, + hint="Inspect: journalctl -u live-console.service -n 120 --no-pager", + ) + if env_flag(env, "ENABLE_LIVE_EDGE_PROXY", True): + ok, detail = systemd_active("live-edge.service") + check( + checks, + "live_edge_service", + "live-edge.service", + ok, + detail=detail, + hint="Run scripts/linux/reinstall_live_edge.sh or restart live-edge.service.", + ) + ok, detail = systemd_active("live-edge-watch.service") + check( + checks, + "live_edge_watch_service", + "live-edge-watch.service", + ok, + detail=detail, + hint="Run scripts/linux/reinstall_live_edge.sh to install the live.local watchdog.", + ) + + +def verify_http(checks: list[dict[str, Any]], env: dict[str, str], *, timeout: float) -> None: + port = env.get("PORT", os.environ.get("PORT", "8001")) + mdns_host = f"{env.get('HOSTNAME_ALIAS', 'live')}.local" + ok, code, data, err, latency = http_json(f"http://127.0.0.1:{port}/health", timeout=timeout) + check( + checks, + "api_health", + "FastAPI /health", + ok and data.get("status") == "ok", + detail=f"HTTP {code}, {latency} ms" if code else err, + hint="live-console is not serving the control plane.", + data={"url": f"http://127.0.0.1:{port}/health"}, + ) + ok, code, data, err, latency = http_json(f"http://127.0.0.1:{port}/deploy/check", timeout=timeout + 4) + check( + checks, + "deploy_check", + "FastAPI /deploy/check", + ok and bool(data.get("ready")), + detail=( + f"{'READY' if data.get('ready') else 'DEGRADED'} " + f"({data.get('pass_count', 0)}/{data.get('total_count', 0)}), {latency} ms" + if code + else err + ), + hint="Use deploy/check failing_checks for the exact live.local or edge proxy failure.", + data={"url": f"http://127.0.0.1:{port}/deploy/check", "report": data}, + ) + if env_flag(env, "ENABLE_LIVE_EDGE_PROXY", True): + ok_b, code_b, _body, err_b, latency_b = http_bytes("http://127.0.0.1/health", timeout=timeout) + check( + checks, + "edge_health", + "live-edge /health", + ok_b and code_b == 200, + detail=f"HTTP {code_b}, {latency_b} ms" if code_b else err_b, + hint="Port 80 should serve http://live.local through live-edge.service.", + data={"url": "http://127.0.0.1/health", "mdns_url": f"http://{mdns_host}/"}, + ) + + +def neko_required_count(env: dict[str, str]) -> int: + max_count = max(2, min(64, env_int(env, "NEKO_MAX_INSTANCE_COUNT", 64))) + return max(2, min(max_count, env_int(env, "NEKO_INSTANCE_COUNT", 2))) + + +def verify_neko(checks: list[dict[str, Any]], env: dict[str, str], *, timeout: float) -> None: + if not env_flag(env, "ENABLE_NEKO", True): + check(checks, "neko_enabled", "Neko enabled", True, required=False, detail="disabled") + return + docker_ok, names, err = docker_ps_names() + check( + checks, + "docker_daemon", + "Docker daemon", + docker_ok, + detail="running" if docker_ok else err, + hint="Start Docker before Neko/Redroid/SRS services.", + ) + if not docker_ok: + return + ok, detail = systemd_active("live-neko-ip-watch.service") + check( + checks, + "neko_ip_watch_service", + "live-neko-ip-watch.service", + ok, + detail=detail, + hint="Run scripts/linux/upgrade_live_console.sh or reinstall the Neko LAN IP watcher.", + ) + required = neko_required_count(env) + running = [name for name in names if name.startswith("live-neko-")] + check( + checks, + "neko_containers", + "Neko containers", + len(running) >= required, + detail=f"running={len(running)} required={required}: {', '.join(running) or '-'}", + hint="Run sudo bash scripts/linux/service_ctl.sh neko restart.", + ) + for idx in range(1, required + 1): + name = f"live-neko-{idx}" + if name not in running: + continue + code, out, err2 = docker_exec( + name, + "supervisorctl status google-chrome 2>/dev/null || supervisorctl status chromium 2>/dev/null", + timeout=timeout, + ) + check( + checks, + f"neko_{idx}_browser", + f"{name} browser process", + code == 0 and "RUNNING" in out, + detail=(out or err2).strip(), + hint="Make services/neko/browser-launch.sh executable and restart Neko.", + ) + code, out, err2 = docker_exec(name, "supervisorctl status tampermonkey-provisioning", timeout=timeout) + check( + checks, + f"neko_{idx}_tm_server", + f"{name} Tampermonkey provisioning server", + code == 0 and "RUNNING" in out, + detail=(out or err2).strip(), + hint="The Neko supervisor must run tampermonkey-provisioning on 127.0.0.1:18080.", + ) + code, out, err2 = docker_exec( + name, + "curl -fsS http://127.0.0.1:18080/tampermonkey-provisioning.json | grep -q 'YouTube Studio Auto Dismiss'", + timeout=timeout, + ) + check( + checks, + f"neko_{idx}_tm_payload", + f"{name} Tampermonkey default script payload", + code == 0, + detail="payload contains YouTube Studio Auto Dismiss" if code == 0 else (err2 or out).strip(), + hint="Regenerate services/neko/provisioning/tampermonkey-provisioning.json and restart Neko.", + ) + code, out, err2 = docker_exec( + name, + "grep -R -a -l 'YouTube Studio Auto Dismiss' /home/neko/.config/google-chrome/Default /home/neko/.config/chromium/Default 2>/dev/null | head -1", + timeout=timeout, + ) + check( + checks, + f"neko_{idx}_tm_storage", + f"{name} Tampermonkey script imported", + code == 0 and bool(out.strip()), + detail=out.strip() or err2.strip() or "not found", + hint="Open Neko once or restart the browser so Chrome policy imports the provisioning payload.", + ) + + +def verify_youtube_pro_capacity( + checks: list[dict[str, Any]], + env: dict[str, str], + *, + port: str, + timeout: float, +) -> None: + ok, code, data, err, latency = http_json(f"http://127.0.0.1:{port}/youtube_pro_channels", timeout=timeout) + channels = data.get("channels") if isinstance(data, dict) else [] + if not isinstance(channels, list): + channels = [] + desired = max(2, min(64, len(channels) or 2)) + current = neko_required_count(env) + check( + checks, + "youtube_pro_neko_capacity", + "YouTube PRO Neko capacity", + ok and current >= desired, + required=env_flag(env, "ENABLE_NEKO", True), + detail=( + f"HTTP {code}, channels={len(channels)}, neko={current}, required={desired}, {latency} ms" + if code + else err + ), + hint="Saving YouTube PRO channels should raise NEKO_INSTANCE_COUNT and restart Neko.", + ) + + +def verify_android(checks: list[dict[str, Any]], env: dict[str, str], *, port: str, timeout: float) -> None: + ok, code, data, err, latency = http_json(f"http://127.0.0.1:{port}/android/devices", timeout=timeout) + devices = data.get("devices") if isinstance(data, dict) else [] + if not isinstance(devices, list): + devices = [] + panel_required = env_flag(env, "ENABLE_ANDROID_PANEL", True) + check( + checks, + "android_devices", + "Android device API", + ok and 200 <= code < 300 and isinstance(data, dict), + required=panel_required, + detail=f"HTTP {code}, devices={len(devices)}, {latency} ms" if code else err, + hint="The control plane must expose /android/devices for Redroid and physical ADB devices.", + ) + check( + checks, + "android_device_presence", + "ADB Android device presence", + bool(data.get("available")) and bool(devices), + required=False, + detail=f"devices={len(devices)}", + hint="Run adb devices -l; for Redroid run scripts/linux/service_ctl.sh redroid start.", + ) + if not devices: + return + serial = str(devices[0].get("serial") or "") + if not serial: + return + query = urllib.parse.urlencode({"serial": serial, "fast": "1"}) + ok_b, code_b, body, err_b, latency_b = http_bytes( + f"http://127.0.0.1:{port}/android/screenshot?{query}", + timeout=max(timeout, 12.0), + ) + check( + checks, + "android_screenshot", + "Android screenshot PNG", + ok_b and code_b == 200 and body.startswith(PNG_SIG), + required=True, + detail=f"HTTP {code_b}, bytes={len(body)}, {latency_b} ms" if code_b else err_b, + hint="The Web panel needs a valid PNG fallback even when scrcpy/WebCodecs fails.", + data={"serial": serial}, + ) + ok_a, code_a, action_data, err_a, latency_a = http_json_post( + f"http://127.0.0.1:{port}/android/action", + {"serial": serial, "action": "wake", "payload": {}}, + timeout=max(timeout, 12.0), + ) + check( + checks, + "android_action", + "Android web control action", + ok_a and code_a == 200 and isinstance(action_data, dict) and not action_data.get("error"), + required=True, + detail=( + f"HTTP {code_a}, {action_data.get('message', 'action ok')}, {latency_a} ms" + if code_a + else err_a + ), + hint="The Web panel touch path requires /android/action to send ADB input events.", + data={"serial": serial}, + ) + + +def summarize(checks: list[dict[str, Any]]) -> dict[str, Any]: + required = [item for item in checks if item.get("required", True)] + failed = [item for item in required if not item.get("ok")] + return { + "ready": not failed, + "pass_count": sum(1 for item in checks if item.get("ok")), + "total_count": len(checks), + "required_total": len(required), + "checks": checks, + "failing_checks": failed, + "hints": [item["hint"] for item in failed if item.get("hint")], + } + + +def print_human(report: dict[str, Any]) -> None: + status = "READY" if report["ready"] else "DEGRADED" + print(f"SHD2YPP2 local verification: {status} ({report['pass_count']}/{report['total_count']})") + for item in report["checks"]: + mark = "OK" if item.get("ok") else ("FAIL" if item.get("required", True) else "WARN") + req = "required" if item.get("required", True) else "optional" + print(f"{mark:4} {item['id']:28} {req:8} {item['detail']}") + if report["hints"]: + print("Hints:") + for hint in report["hints"]: + print(f"- {hint}") + + +def build_report(args: argparse.Namespace) -> dict[str, Any]: + install_dir = Path(args.install_dir).resolve() + env = read_env(install_dir / "config" / "system-stack.env") + port = str(args.port or env.get("PORT") or os.environ.get("PORT") or "8001") + checks: list[dict[str, Any]] = [] + verify_files(checks, install_dir) + verify_systemd(checks, env) + verify_http(checks, env, timeout=args.timeout) + if not args.skip_neko: + verify_neko(checks, env, timeout=args.timeout) + verify_youtube_pro_capacity(checks, env, port=port, timeout=args.timeout) + if not args.skip_android: + verify_android(checks, env, port=port, timeout=args.timeout) + report = summarize(checks) + report.update( + { + "install_dir": str(install_dir), + "port": port, + "mdns_host": f"{env.get('HOSTNAME_ALIAS', 'live')}.local", + } + ) + return report + + +def main() -> int: + parser = argparse.ArgumentParser(description="Verify a local SHD2YPP2 installation") + parser.add_argument("--install-dir", default=str(DEFAULT_INSTALL_DIR), help="Runtime project directory") + parser.add_argument("--port", default="", help="Control plane port; defaults to system-stack.env PORT") + parser.add_argument("--timeout", type=float, default=5.0, help="HTTP and command timeout seconds") + parser.add_argument("--skip-neko", action="store_true", help="Skip Docker/Neko/Tampermonkey checks") + parser.add_argument("--skip-android", action="store_true", help="Skip Android/ADB screenshot checks") + parser.add_argument("--json", action="store_true", help="Print JSON report") + args = parser.parse_args() + + report = build_report(args) + if args.json: + print(json.dumps(report, ensure_ascii=False, indent=2)) + else: + print_human(report) + return 0 if report["ready"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/d2ypp2/web-console/components/live-product/LiveAndroidStreamConsole.tsx b/d2ypp2/web-console/components/live-product/LiveAndroidStreamConsole.tsx index 9117f6c..86d66ab 100644 --- a/d2ypp2/web-console/components/live-product/LiveAndroidStreamConsole.tsx +++ b/d2ypp2/web-console/components/live-product/LiveAndroidStreamConsole.tsx @@ -39,6 +39,8 @@ export type DeviceRow = { type TFn = (zh: string, en: string) => string; const BOOKMARKS_KEY = "live-hub-adb-bookmarks-v1"; +const SCREENSHOT_FALLBACK_INTERVAL_MS = 450; +const SCRCPY_FIRST_FRAME_TIMEOUT_MS = 5200; function clientPointToCanvasPixel( canvas: HTMLCanvasElement, @@ -195,6 +197,9 @@ export function LiveAndroidStreamConsole({ const decoderRef = useRef(null); const mirrorFocusRef = useRef(null); const shotFailRef = useRef(0); + const scrcpyExpectedCloseRef = useRef(false); + const scrcpyFirstFrameTimerRef = useRef(null); + const scrcpyGotFrameRef = useRef(false); const activeTransport = devices.find((d) => d.serial === serial)?.transport; const useFastShot = liveMs > 0; @@ -207,6 +212,13 @@ export function LiveAndroidStreamConsole({ setFrameTs((n) => n + 1); }, []); + const clearScrcpyFirstFrameTimer = useCallback(() => { + if (scrcpyFirstFrameTimerRef.current != null) { + window.clearTimeout(scrcpyFirstFrameTimerRef.current); + scrcpyFirstFrameTimerRef.current = null; + } + }, []); + useEffect(() => { const onVis = () => setPageVisible(document.visibilityState === "visible"); onVis(); @@ -304,6 +316,7 @@ export function LiveAndroidStreamConsole({ useEffect(() => { setScrcpyUserStoppedForSerial(null); + scrcpyExpectedCloseRef.current = false; }, [serial]); useEffect(() => { @@ -343,6 +356,9 @@ export function LiveAndroidStreamConsole({ useEffect(() => { if (!scrcpyStream || !serial || !adbAvailable) { + clearScrcpyFirstFrameTimer(); + scrcpyExpectedCloseRef.current = true; + scrcpyGotFrameRef.current = false; wsRef.current?.close(); wsRef.current = null; decoderRef.current?.reset(); @@ -358,9 +374,29 @@ export function LiveAndroidStreamConsole({ const canvas = canvasRef.current; if (!canvas) return; - const dec = new ScrcpyH264Decoder(canvas, (msg) => setScrcpyErr(msg)); + const fallbackToScreenshot = (message: string) => { + clearScrcpyFirstFrameTimer(); + scrcpyExpectedCloseRef.current = true; + wsRef.current?.close(); + setScrcpyErr(message); + setScrcpyStream(false); + setLiveMs(SCREENSHOT_FALLBACK_INTERVAL_MS); + setScrcpyUserStoppedForSerial(serial); + window.setTimeout(() => bumpFrame(), 120); + }; + + const dec = new ScrcpyH264Decoder( + canvas, + (msg) => fallbackToScreenshot(`${t("实时画面解码失败,已切回截图模式", "Live stream decode failed, using screenshot mode")}: ${msg}`), + () => { + scrcpyGotFrameRef.current = true; + clearScrcpyFirstFrameTimer(); + }, + ); decoderRef.current = dec; + scrcpyExpectedCloseRef.current = false; + scrcpyGotFrameRef.current = false; const wsUrl = androidScrcpyWsUrl(apiBase, serial); const ws = new WebSocket(wsUrl); wsRef.current = ws; @@ -369,6 +405,11 @@ export function LiveAndroidStreamConsole({ ws.onopen = () => { setScrcpyConnected(true); setScrcpyErr(null); + clearScrcpyFirstFrameTimer(); + scrcpyFirstFrameTimerRef.current = window.setTimeout(() => { + if (scrcpyGotFrameRef.current) return; + fallbackToScreenshot(t("实时画面无首帧,已切回截图模式", "No first live frame, using screenshot mode")); + }, SCRCPY_FIRST_FRAME_TIMEOUT_MS); }; ws.onmessage = (ev) => { if (typeof ev.data === "string") { @@ -389,15 +430,22 @@ export function LiveAndroidStreamConsole({ setScrcpyConnected(false); decoderRef.current?.reset(); decoderRef.current = null; + clearScrcpyFirstFrameTimer(); + if (scrcpyStream && !scrcpyExpectedCloseRef.current) { + fallbackToScreenshot(t("实时画面连接失败,已切回截图模式", "Live stream failed, using screenshot mode")); + } }; return () => { + clearScrcpyFirstFrameTimer(); + scrcpyExpectedCloseRef.current = true; + scrcpyGotFrameRef.current = false; ws.close(); wsRef.current = null; dec.reset(); if (decoderRef.current === dec) decoderRef.current = null; }; - }, [scrcpyStream, serial, adbAvailable, apiBase, t]); + }, [scrcpyStream, serial, adbAvailable, apiBase, bumpFrame, clearScrcpyFirstFrameTimer, t]); useEffect(() => { if (serial && adbAvailable) { diff --git a/d2ypp2/web-console/components/live-product/LiveControlApp.tsx b/d2ypp2/web-console/components/live-product/LiveControlApp.tsx index 71c9afd..678ec6e 100644 --- a/d2ypp2/web-console/components/live-product/LiveControlApp.tsx +++ b/d2ypp2/web-console/components/live-product/LiveControlApp.tsx @@ -118,9 +118,17 @@ type HubDashboard = { tx_bytes?: number; rx_packets?: number; tx_packets?: number; + rx_bps?: number; + tx_bps?: number; + sample_interval_seconds?: number; speed_mbps?: number | null; operstate?: string; }>; + total_rx_bytes?: number; + total_tx_bytes?: number; + total_rx_bps?: number; + total_tx_bps?: number; + sample_interval_seconds?: number; }; live_events?: { pm2_tail?: string[]; @@ -221,6 +229,11 @@ function formatBytes(value?: number) { return `${n.toFixed(n >= 10 || i === 0 ? 0 : 1)} ${u[i]}`; } +function formatRate(value?: number) { + const text = formatBytes(value); + return text === "—" ? text : `${text}/s`; +} + function statusPill(status: string) { if (status === "online" || status === "running") return "bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30"; @@ -1181,11 +1194,26 @@ export default function LiveControlApp() {

- {t("网络流量(累计)", "Network I/O (cumulative)")} + {t("网络带宽 / 流量", "Network bandwidth / traffic")}

- {t("自开机以来各接口收发字节;非实时速率。", "Per-interface RX/TX since boot (not a live bitrate).")} + {t( + "实时速率由总览自动刷新间隔计算;累计值来自系统网卡计数器。", + "Live rates are calculated from dashboard refresh deltas; totals come from kernel counters.", + )}

+
+
+

{t("实时下行", "Live down")}

+

{formatRate(dash.network.total_rx_bps)}

+

{t("累计", "Total")}: {formatBytes(dash.network.total_rx_bytes)}

+
+
+

{t("实时上行", "Live up")}

+

{formatRate(dash.network.total_tx_bps)}

+

{t("累计", "Total")}: {formatBytes(dash.network.total_tx_bytes)}

+
+
{(dash.network.interfaces || []).slice(0, 6).map((iface) => (

- ↓ {formatBytes(iface.rx_bytes)} ·{" "} + ↓ {formatRate(iface.rx_bps)} ·{" "} + {formatBytes(iface.rx_bytes)} ·{" "} {iface.rx_packets?.toLocaleString() ?? "—"} pkt

- ↑ {formatBytes(iface.tx_bytes)} ·{" "} + ↑ {formatRate(iface.tx_bps)} ·{" "} + {formatBytes(iface.tx_bytes)} ·{" "} {iface.tx_packets?.toLocaleString() ?? "—"} pkt

diff --git a/d2ypp2/web-console/lib/scrcpyH264Decoder.ts b/d2ypp2/web-console/lib/scrcpyH264Decoder.ts index c514975..cf541bf 100644 --- a/d2ypp2/web-console/lib/scrcpyH264Decoder.ts +++ b/d2ypp2/web-console/lib/scrcpyH264Decoder.ts @@ -53,6 +53,7 @@ export class ScrcpyH264Decoder { constructor( private readonly canvas: HTMLCanvasElement, private readonly onDecodeError: (msg: string) => void, + private readonly onFrame?: () => void, ) {} reset(): void { @@ -146,6 +147,7 @@ export class ScrcpyH264Decoder { } ctx.drawImage(frame, 0, 0); frame.close(); + this.onFrame?.(); }, error: (e) => this.onDecodeError(String((e as DOMException).message || e)), }); diff --git a/d2ypp2/web-console/out/.build-stamp.json b/d2ypp2/web-console/out/.build-stamp.json index c5acb9c..8fffbee 100644 --- a/d2ypp2/web-console/out/.build-stamp.json +++ b/d2ypp2/web-console/out/.build-stamp.json @@ -1,6 +1,6 @@ { - "built_at": 1779025482.4990735, - "source_mtime": 1779025347.1360698, + "built_at": 1779172552.256737, + "source_mtime": 1779162468.269062, "toolchain": "local", "node_major": 20 } \ No newline at end of file diff --git a/d2ypp2/web-console/out/404.html b/d2ypp2/web-console/out/404.html index f3d8fcf..6d5833a 100644 --- a/d2ypp2/web-console/out/404.html +++ b/d2ypp2/web-console/out/404.html @@ -1 +1 @@ -404: This page could not be found.无人直播系统 · live.local

404

This page could not be found.

\ No newline at end of file +404: This page could not be found.无人直播系统 · live.local

404

This page could not be found.

\ No newline at end of file diff --git a/d2ypp2/web-console/out/_next/static/chunks/app/page-10361036b8bf1ca2.js b/d2ypp2/web-console/out/_next/static/chunks/app/page-10361036b8bf1ca2.js new file mode 100644 index 0000000..4938bcb --- /dev/null +++ b/d2ypp2/web-console/out/_next/static/chunks/app/page-10361036b8bf1ca2.js @@ -0,0 +1 @@ +(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{4578:function(e,t,s){Promise.resolve().then(s.bind(s,1499))},1499:function(e,t,s){"use strict";s.r(t),s.d(t,{default:function(){return e8}});var n=s(7437),l=s(6247),i=s(7586),r=s(4401),a=s(6137),o=s(4372),c=s(5901),d=s(3388),x=s(3420),u=s(8728),m=s(2023),h=s(407),p=s(6840),b=s(7226),g=s(5929),f=s(7945),v=s(1817),y=s(7168),w=s(4822),N=s(4428),j=s(1723),k=s(2926),z=s(7580),S=s(2489),_=s(2265);function C(e){let{tr:t}=e,l=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let e;if(!l.current)return;let t=!1,n=0;return s.e(689).then(s.bind(s,1448)).then(s=>{if(t||!l.current)return;let i=l.current,r=Math.max(i.clientWidth,280),a=Math.max(i.clientHeight,200),o=new s.Scene,c=new s.PerspectiveCamera(48,r/a,.1,80);c.position.set(0,1.1,7.2);let d=new s.WebGLRenderer({antialias:!0,alpha:!0});d.setPixelRatio(Math.min(window.devicePixelRatio,2)),d.setSize(r,a),d.setClearColor(0,0),i.appendChild(d.domElement);let x=new s.HemisphereLight(16762092,4482815,.85);o.add(x);let u=new s.DirectionalLight(16777215,.9);u.position.set(4,8,6),o.add(u);let m=new s.PointLight(8189439,.6,20);m.position.set(-3,2,-2),o.add(m);let h=new s.Group,p=new s.Mesh(new s.BoxGeometry(3.2,.35,2.2),new s.MeshStandardMaterial({color:2762048,metalness:.15,roughness:.55}));p.position.y=-.85,h.add(p);let b=new s.Mesh(new s.TorusGeometry(.72,.09,12,48),new s.MeshStandardMaterial({color:16724821,emissive:4456465,metalness:.35,roughness:.25}));b.rotation.x=Math.PI/2,b.position.set(-.85,.15,.2),h.add(b);let g=new s.Mesh(new s.ConeGeometry(.35,.5,3),new s.MeshStandardMaterial({color:16777215,emissive:2236962,metalness:.2,roughness:.3}));g.rotation.z=-Math.PI/2,g.rotation.y=Math.PI,g.position.set(-.55,.18,.2),h.add(g);let f=new s.Mesh(new s.CylinderGeometry(.22,.28,.95,20),new s.MeshStandardMaterial({color:2487534,emissive:340024,metalness:.25,roughness:.35}));f.position.set(1.05,.35,-.15),f.rotation.z=.12,h.add(f);let v=new s.Mesh(new s.SphereGeometry(.26,16,16),new s.MeshStandardMaterial({color:16657493,emissive:2229520,metalness:.2,roughness:.4}));v.position.set(1.05,.92,-.15),h.add(v);let y=new s.BufferGeometry,w=new Float32Array(360);for(let e=0;e<120;e++)w[3*e]=(Math.random()-.5)*14,w[3*e+1]=(Math.random()-.3)*8,w[3*e+2]=(Math.random()-.5)*8-2;y.setAttribute("position",new s.BufferAttribute(w,3));let N=new s.Points(y,new s.PointsMaterial({color:16777215,size:.06,transparent:!0,opacity:.7}));o.add(N),o.add(h);let j=performance.now(),k=()=>{if(t)return;n=requestAnimationFrame(k);let e=(performance.now()-j)/1e3;h.rotation.y=.25*Math.sin(.35*e)+.12*e,h.position.y=.06*Math.sin(.9*e),b.rotation.z=.4*e,N.rotation.y=.02*e,d.render(o,c)};k();let z=new ResizeObserver(()=>{if(!l.current||t)return;let e=Math.max(l.current.clientWidth,280),s=Math.max(l.current.clientHeight,200);c.aspect=e/s,c.updateProjectionMatrix(),d.setSize(e,s)});z.observe(i),e=()=>{cancelAnimationFrame(n),z.disconnect(),d.dispose(),y.dispose(),d.domElement.parentNode===i&&i.removeChild(d.domElement)}}),()=>{t=!0,cancelAnimationFrame(n),null==e||e()}},[]),(0,n.jsxs)("div",{className:"flex h-full min-h-[14rem] flex-col",children:[(0,n.jsx)("p",{className:"mb-2 px-1 text-[10px] font-medium uppercase tracking-wider text-fuchsia-200/80",children:t("直播工作室 \xb7 3D","Live studio \xb7 3D")}),(0,n.jsx)("div",{ref:l,className:"relative min-h-[12rem] flex-1 overflow-hidden rounded-xl bg-gradient-to-b from-fuchsia-950/40 to-cyan-950/20 ring-1 ring-white/10"})]})}var R=s(9170),P=s(2246),L=s(4286),M=s(4794),T=s(221),U=s(8867),E=s(8124),I=s(2101),D=s(3651),A=s(9985),Z=s(6362),O=s(3468),H=s(6443),B=s(5287),K=s(3276),F=s(9227),W=s(257),Y=s(1387);class J{reset(){if(this.buf=new Uint8Array(0),this.sps=null,this.pps=null,this.configured=!1,this.configuring=!1,this.ts=0,this.decoder){try{this.decoder.close()}catch(e){}this.decoder=null}}push(e){let t=new Uint8Array(e.byteLength);t.set(new Uint8Array(e)),this.buf=function(e,t){let s=new Uint8Array(e.length+t.length);return s.set(e,0),s.set(t,e.length),s}(this.buf,t),this.drainAvcc()}drainAvcc(){let e=this.buf,t=0;for(;t+4<=e.length;){var s;let n=e[s=t]<<24|e[s+1]<<16|e[s+2]<<8|e[s+3];if(n<=0||n>6291456){this.buf=new Uint8Array(e.subarray(t+1)),t=0;continue}if(t+4+n>e.length)break;let l=e.subarray(t+4,t+4+n);t+=4+n,this.handleNal(l)}t>0&&(this.buf=new Uint8Array(e.subarray(t)))}handleNal(e){if(e.length<2)return;let t=31&e[0];if(7===t?this.sps=new Uint8Array(e):8===t&&(this.pps=new Uint8Array(e)),this.sps&&this.pps&&!this.configured&&!this.configuring&&this.configure(),!this.configured||!this.decoder||1!==t&&5!==t&&7!==t&&8!==t||7===t||8===t)return;let s=new Uint8Array(4+e.length);new DataView(s.buffer).setUint32(0,e.length,!1),s.set(e,4);try{let e=new EncodedVideoChunk({type:5===t?"key":"delta",timestamp:this.ts,data:s});this.ts+=33333,this.decoder.decode(e)}catch(e){this.onDecodeError(e.message||String(e))}}async configure(){if(this.sps&&this.pps&&!this.configured&&!this.configuring){this.configuring=!0;try{let e=function(e,t){let s=e.subarray(1),n=t.subarray(1),l=new Uint8Array(11+s.length+3+n.length),i=0;return l[i++]=1,l[i++]=e[1],l[i++]=e[2],l[i++]=e[3],l[i++]=255,l[i++]=225,l[i++]=s.length>>8&255,l[i++]=255&s.length,l.set(s,i),i+=s.length,l[i++]=1,l[i++]=n.length>>8&255,l[i++]=255&n.length,l.set(n,i),l}(this.sps,this.pps),t=function(e){let t=e=>e.toString(16).padStart(2,"0").toUpperCase();return"avc1.".concat(t(e[1])).concat(t(e[2])).concat(t(e[3]))}(this.sps),s=new VideoDecoder({output:e=>{var t;let s=this.canvas.getContext("2d");if(!s){e.close();return}(this.canvas.width!==e.displayWidth||this.canvas.height!==e.displayHeight)&&(this.canvas.width=e.displayWidth,this.canvas.height=e.displayHeight),s.drawImage(e,0,0),e.close(),null===(t=this.onFrame)||void 0===t||t.call(this)},error:e=>this.onDecodeError(String(e.message||e))});await s.configure({codec:t,description:e,optimizeForLatency:!0}),this.decoder=s,this.configured=!0}catch(e){this.onDecodeError(e.message||String(e))}finally{this.configuring=!1}}}constructor(e,t,s){this.canvas=e,this.onDecodeError=t,this.onFrame=s,this.buf=new Uint8Array(0),this.decoder=null,this.configured=!1,this.configuring=!1,this.ts=0,this.sps=null,this.pps=null}}function G(){return"undefined"!=typeof VideoDecoder&&"undefined"!=typeof VideoFrame}let V="live-hub-adb-bookmarks-v1";function q(e,t,s){let n,l,i,r;let a=e.getBoundingClientRect(),o=e.width,c=e.height;if(!o||!c)return null;let d=a.width/a.height,x=o/c;x>d?(n=a.width,l=a.width/x,i=0,r=(a.height-l)/2):(l=a.height,n=a.height*x,i=(a.width-n)/2,r=0);let u=t-a.left-i,m=s-a.top-r;return u<0||m<0||u>n||m>l?null:{x:Math.max(0,Math.min(o-1,Math.round(u/n*o))),y:Math.max(0,Math.min(c-1,Math.round(m/l*c)))}}function X(e,t,s){let n,l,i,r;let a=e.getBoundingClientRect(),o=e.naturalWidth,c=e.naturalHeight;if(!o||!c)return null;let d=a.width/a.height,x=o/c;x>d?(n=a.width,l=a.width/x,i=0,r=(a.height-l)/2):(l=a.height,n=a.height*x,i=(a.width-n)/2,r=0);let u=t-a.left-i,m=s-a.top-r;return u<0||m<0||u>n||m>l?null:{x:Math.max(0,Math.min(o-1,Math.round(u/n*o))),y:Math.max(0,Math.min(c-1,Math.round(m/l*c)))}}let $=[0,280,450,700,1100];function Q(e){switch(e){case"usb":return{Icon:I.Z,zh:"USB",en:"USB",cls:"bg-amber-500/20 text-amber-100 ring-amber-500/35"};case"tcp":return{Icon:k.Z,zh:"网络",en:"TCP",cls:"bg-cyan-500/20 text-cyan-100 ring-cyan-500/35"};case"emulator":return{Icon:D.Z,zh:"模拟器",en:"Emu",cls:"bg-fuchsia-500/20 text-fuchsia-100 ring-fuchsia-500/35"};default:return{Icon:d.Z,zh:"?",en:"?",cls:"bg-zinc-600/30 text-zinc-400"}}}let ee="https://github.com/Genymobile/scrcpy",et="https://github.com/viarotel-org/escrcpy";function es(e,t){let s=t.startsWith("/")?t:"/".concat(t),n=(e||"").trim().replace(/\/$/,"");return n?"".concat(n).concat(s):s}function en(e){var t;let{t:s,apiBase:l,fetchJson:i,serial:r,devices:a,setSerial:o,adbAvailable:c,lock:x,overview:u,onAndroidAction:h,notify:p,onRefreshDevices:b}=e,[g,f]=(0,_.useState)(0),[v,w]=(0,_.useState)(450),[N,j]=(0,_.useState)(!1),[k,z]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[R,P]=(0,_.useState)(!0),[L,M]=(0,_.useState)(""),[T,U]=(0,_.useState)(""),[E,I]=(0,_.useState)("37777"),[D,en]=(0,_.useState)(""),[el,ei]=(0,_.useState)(!1),[er,ea]=(0,_.useState)([]),[eo,ec]=(0,_.useState)(!1),[ed,ex]=(0,_.useState)(null),[eu,em]=(0,_.useState)(!1),[eh,ep]=(0,_.useState)(null),[eb,eg]=(0,_.useState)(null),[ef,ev]=(0,_.useState)(!0),ey=(0,_.useRef)(null),ew=(0,_.useRef)(null),eN=(0,_.useRef)(null),ej=(0,_.useRef)(null),ek=(0,_.useRef)(null),ez=(0,_.useRef)(null),eS=(0,_.useRef)(0),e_=(0,_.useRef)(!1),eC=(0,_.useRef)(null),eR=(0,_.useRef)(!1),eP=null===(t=a.find(e=>e.serial===r))||void 0===t?void 0:t.transport,eL=v>0,eM=r?"".concat(es(l,"/android/screenshot"),"?serial=").concat(encodeURIComponent(r),"&ts=").concat(g).concat(eL?"&fast=1":""):"",eT=(0,_.useCallback)(()=>{f(e=>e+1)},[]),eU=(0,_.useCallback)(()=>{null!=eC.current&&(window.clearTimeout(eC.current),eC.current=null)},[]);(0,_.useEffect)(()=>{let e=()=>ev("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(V);if(e){let t=JSON.parse(e);Array.isArray(t)&&ea(t.map(String).filter(Boolean).slice(0,24))}}catch(e){}},[]);let eE=e=>{let t=e.trim();if(!t)return;let n=[t,...er.filter(e=>e!==t)].slice(0,16);ea(n);try{window.localStorage.setItem(V,JSON.stringify(n))}catch(e){}p(s("已保存快捷地址","Saved quick address"))};(0,_.useEffect)(()=>{if(!r||!c){C(null);return}let e=!1;return(async()=>{try{let t=await (0,Y.N)("".concat(es(l,"/android/display_metrics"),"?serial=").concat(encodeURIComponent(r)));if(e)return;"number"==typeof t.width&&"number"==typeof t.height&&C({width:t.width,height:t.height})}catch(e){}})(),()=>{e=!0}},[l,c,r]),(0,_.useEffect)(()=>{if(!c||!r||v<=0||eo||!ef)return;let e=window.setInterval(()=>f(e=>e+1),v);return()=>clearInterval(e)},[c,v,ef,r,eo]),(0,_.useEffect)(()=>{ef&&c&&r&&!eo&&eT()},[c,eT,ef,r,eo]),(0,_.useEffect)(()=>{if(!c){eg(null);return}let e=!1;return(async()=>{try{let t=await (0,Y.N)(es(l,"/android/scrcpy/info"));e||eg(t)}catch(e){}})(),()=>{e=!0}},[l,c]),(0,_.useEffect)(()=>{eo||ep(null)},[eo]),(0,_.useEffect)(()=>{ex(null),e_.current=!1},[r]),(0,_.useEffect)(()=>{let e=a.find(e=>e.serial===r),t=!!(r&&ed===r);if(c&&r&&(null==e?void 0:e.state)==="device"&&(null==eb?void 0:eb.server_jar)&&G()&&!t){ec(!0),w(0);return}if(t||!r||!c||eb&&!eb.server_jar||!G()||e&&"device"!==e.state){ec(!1);return}},[r,a,c,eb,ed]),(0,_.useEffect)(()=>{if(!eo||!r||!c){var e,t;eU(),e_.current=!0,eR.current=!1,null===(e=ej.current)||void 0===e||e.close(),ej.current=null,null===(t=ek.current)||void 0===t||t.reset(),ek.current=null,em(!1);return}if(!G()){ep(s("当前浏览器不支持 WebCodecs H.264","WebCodecs H.264 not supported in this browser")),ec(!1);return}let n=eN.current;if(!n)return;let i=e=>{var t;eU(),e_.current=!0,null===(t=ej.current)||void 0===t||t.close(),ep(e),ec(!1),w(450),ex(r),window.setTimeout(()=>eT(),120)},a=new J(n,e=>i("".concat(s("实时画面解码失败,已切回截图模式","Live stream decode failed, using screenshot mode"),": ").concat(e)),()=>{eR.current=!0,eU()});ek.current=a,e_.current=!1,eR.current=!1;let o=new WebSocket(function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1920,n=(function(e){let t=(e||"").trim();if(t)try{let e=new URL(t,window.location.href),s="https:"===e.protocol?"wss:":"ws:";return"".concat(s,"//").concat(e.host)}catch(e){}let s=void 0!==W&&W.env.NEXT_PUBLIC_API_URL?String(W.env.NEXT_PUBLIC_API_URL).trim():"";if(s)try{let e=new URL(s,window.location.href),t="https:"===e.protocol?"wss:":"ws:";return"".concat(t,"//").concat(e.host)}catch(e){}let n=window.location.port;if("3000"===n||"3001"===n){let e=window.location.hostname||"127.0.0.1";return"ws://".concat(e,":").concat("8001")}let{protocol:l,host:i}=window.location;return"".concat("https:"===l?"wss:":"ws:","//").concat(i)})(e).replace(/\/$/,""),l=new URLSearchParams({serial:t,max_size:String(s)});return"".concat(n,"/android/scrcpy/ws?").concat(l.toString())}(l,r));return ej.current=o,o.binaryType="arraybuffer",o.onopen=()=>{em(!0),ep(null),eU(),eC.current=window.setTimeout(()=>{eR.current||i(s("实时画面无首帧,已切回截图模式","No first live frame, using screenshot mode"))},5200)},o.onmessage=e=>{var t;if("string"==typeof e.data){try{let t=JSON.parse(e.data);t.error&&ep(t.error)}catch(e){}return}null===(t=ek.current)||void 0===t||t.push(e.data)},o.onerror=()=>{ep(s("WebSocket 异常","WebSocket error"))},o.onclose=()=>{var e;em(!1),null===(e=ek.current)||void 0===e||e.reset(),ek.current=null,eU(),eo&&!e_.current&&i(s("实时画面连接失败,已切回截图模式","Live stream failed, using screenshot mode"))},()=>{eU(),e_.current=!0,eR.current=!1,o.close(),ej.current=null,a.reset(),ek.current===a&&(ek.current=null)}},[eo,r,c,l,eT,eU,s]),(0,_.useEffect)(()=>{r&&c&&(eS.current=0,f(e=>e+1))},[r,c]);let eI=async()=>{let e=L.trim();if(!e){p(s("填写 host:port","Enter host:port"));return}ei(!0);try{let t=await i("/android/connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({address:e})});p(t.message||(t.ok?"OK":s("连接结果见日志","See message"))),b(),eT()}catch(e){p(e.message)}finally{ei(!1)}},eD=async e=>{ei(!0);try{let t=await i("/android/disconnect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({address:e})});p(t.message||"OK"),b()}catch(e){p(e.message)}finally{ei(!1)}},eA=async()=>{let e=T.trim(),t=parseInt(E,10)||37777,n=D.trim();if(!e||!n){p(s("填写配对 IP 与码","Host + code required"));return}ei(!0);try{let l=await i("/android/pair",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({host:e,port:t,code:n})});p(l.message||(l.ok?s("配对成功后再点连接","Paired — now Connect"):"pair"))}catch(e){p(e.message)}finally{ei(!1)}},eZ=async e=>{if(!R||x||!c||!r)return;let t=ew.current;if(!t)return;let n=X(t,e.clientX,e.clientY);if(!n)return;let l=Date.now(),i=ey.current;i&&l-i.t<320&&28>Math.abs(i.x-n.x)&&28>Math.abs(i.y-n.y)?(ey.current=null,await h("double_tap",{x:n.x,y:n.y}),p(s("双击","Double tap"))):(ey.current={t:l,x:n.x,y:n.y},await h("tap",{x:n.x,y:n.y})),eT()},eO=async e=>{if(e.preventDefault(),x||!c||!r)return;let t=ew.current;if(!t)return;let n=X(t,e.clientX,e.clientY);n&&(await h("long_press",{x:n.x,y:n.y,duration:720}),p(s("长按","Long press")),eT())},eH=async e=>{if(!R||x||!c||!r)return;let t=eN.current;if(!t)return;let n=q(t,e.clientX,e.clientY);if(!n)return;let l=Date.now(),i=ey.current;i&&l-i.t<320&&28>Math.abs(i.x-n.x)&&28>Math.abs(i.y-n.y)?(ey.current=null,await h("double_tap",{x:n.x,y:n.y}),p(s("双击","Double tap"))):(ey.current={t:l,x:n.x,y:n.y},await h("tap",{x:n.x,y:n.y})),eo||eT()},eB=async e=>{if(e.preventDefault(),x||!c||!r)return;let t=eN.current;if(!t)return;let n=q(t,e.clientX,e.clientY);n&&(await h("long_press",{x:n.x,y:n.y,duration:720}),p(s("长按","Long press")),eo||eT())};if(!c)return(0,n.jsx)("div",{className:"rounded-2xl border border-amber-500/25 bg-amber-500/[0.06] p-6 text-center text-sm text-amber-200/90",children:s("宿主机未安装 adb,无法使用 Live 控制台。","adb is required for the Live console.")});let eK=Q(eP),eF=eK.Icon;return(0,n.jsxs)("div",{className:"space-y-5",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-sky-500/20 bg-gradient-to-br from-sky-500/[0.08] via-zinc-950/80 to-zinc-900/90 p-4 ring-1 ring-white/[0.05]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 border-b border-white/[0.06] pb-3",children:[(0,n.jsx)(A.Z,{className:"h-4 w-4 text-sky-400"}),(0,n.jsx)("h4",{className:"text-sm font-semibold text-white",children:s("链路管理","Link manager")}),(0,n.jsx)("span",{className:"text-[10px] text-zinc-500",children:s("USB / Redroid :5555 / 无线调试(先配对再连接)","USB \xb7 Redroid :5555 \xb7 wireless (pair then connect)")})]}),(0,n.jsxs)("div",{className:"mt-3 grid gap-3 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"space-y-2 rounded-xl border border-white/[0.06] bg-black/35 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:"adb connect"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("input",{className:"min-w-[12rem] flex-1 rounded-lg border border-white/10 bg-black/50 px-3 py-2 font-mono text-xs text-zinc-200",placeholder:"192.168.1.10:5555",value:L,onChange:e=>M(e.target.value),onKeyDown:e=>"Enter"===e.key&&void eI()}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eI(),className:"rounded-lg bg-sky-500/25 px-3 py-2 text-xs font-semibold text-sky-100 ring-1 ring-sky-400/40",children:el?"…":"Connect"}),(0,n.jsx)("button",{type:"button",disabled:el||x||!L.trim(),onClick:()=>eE(L),className:"rounded-lg border border-white/10 px-3 py-2 text-xs text-zinc-400 hover:text-zinc-200",children:s("存书签","Pin")})]}),er.length?(0,n.jsx)("div",{className:"flex flex-wrap gap-1.5 pt-1",children:er.map(e=>(0,n.jsx)("button",{type:"button",className:"rounded-md bg-white/[0.06] px-2 py-0.5 font-mono text-[10px] text-zinc-400 hover:bg-white/10",onClick:()=>M(e),children:e},e))}):null,(0,n.jsxs)("div",{className:"flex flex-wrap gap-2 pt-2",children:[(0,n.jsx)("button",{type:"button",disabled:el||x||!r,onClick:()=>void eD(r),className:"rounded-lg border border-rose-500/30 bg-rose-500/10 px-2.5 py-1.5 text-[10px] text-rose-200",children:s("断开当前 TCP","Disconnect current")}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eD(""),className:"rounded-lg border border-white/10 px-2.5 py-1.5 text-[10px] text-zinc-500 hover:text-zinc-300",children:s("断开全部 TCP","Disconnect all TCP")}),(0,n.jsxs)("button",{type:"button",disabled:el||x,onClick:()=>{b(),eT(),p(s("已刷新设备列表","Devices refreshed"))},className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-2.5 py-1.5 text-[10px] text-zinc-400",children:[(0,n.jsx)(y.Z,{className:"h-3 w-3"}),"ADB list"]})]})]}),(0,n.jsxs)("div",{className:"space-y-2 rounded-xl border border-white/[0.06] bg-black/35 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:"adb pair(无线调试)"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("input",{className:"w-32 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs",placeholder:"IP",value:T,onChange:e=>U(e.target.value)}),(0,n.jsx)("input",{className:"w-20 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs",placeholder:"port",value:E,onChange:e=>I(e.target.value)}),(0,n.jsx)("input",{className:"min-w-[6rem] flex-1 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs tracking-widest",placeholder:"123456",value:D,onChange:e=>en(e.target.value)}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eA(),className:"rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs text-violet-100 ring-1 ring-violet-400/35",children:"Pair"})]}),(0,n.jsx)("p",{className:"text-[10px] leading-relaxed text-zinc-600",children:s("手机上「无线调试」会显示配对端口与 6 位码;配对成功后用上方 connect 连接 adb 显示的 IP:5555。","Use the pairing port + 6-digit code from Wireless debugging; then connect to the listed adb IP:5555.")})]})]})]}),(0,n.jsxs)("div",{className:"relative overflow-hidden rounded-3xl border border-emerald-500/25 bg-gradient-to-br from-emerald-500/[0.12] via-zinc-950/90 to-violet-500/[0.08] p-1 shadow-2xl shadow-emerald-900/20 ring-1 ring-white/[0.06]",children:[(0,n.jsx)("div",{className:"pointer-events-none absolute -right-20 -top-20 h-56 w-56 rounded-full bg-emerald-500/10 blur-3xl","aria-hidden":!0}),(0,n.jsxs)("div",{className:"relative rounded-[1.35rem] border border-white/[0.06] bg-zinc-950/80 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between",children:[(0,n.jsxs)("div",{className:"flex items-start gap-3",children:[(0,n.jsx)("div",{className:"flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-400/30 to-violet-500/25 ring-1 ring-white/10",children:(0,n.jsx)(m.Z,{className:"h-6 w-6 text-emerald-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-base font-semibold tracking-tight text-white",children:s("无人直播系统 \xb7 ADB 云控台","Unmanned Live System \xb7 ADB farm console")}),(0,n.jsxs)("p",{className:"mt-1 max-w-xl text-[11px] leading-relaxed text-zinc-500",children:[s("统一适配 USB / Redroid / 无线 adb:已授权设备默认走官方 scrcpy-server 的 WebSocket H.264 真流(与桌面 ","USB / Redroid / wireless adb: authorized devices default to official scrcpy-server WebSocket H.264 (same stack as "),(0,n.jsx)("a",{href:et,target:"_blank",rel:"noreferrer",className:"text-emerald-300/90 underline-offset-2 hover:underline",children:"escrcpy"}),s(" / "," / "),(0,n.jsx)("a",{href:ee,target:"_blank",rel:"noreferrer",className:"text-emerald-300/90 underline-offset-2 hover:underline",children:"scrcpy"}),s(");无 jar 或关闭真流时用截图。","); PNG screenshot when jar missing or stream off.")]})]})]}),(0,n.jsxs)("div",{className:"flex shrink-0 flex-col gap-1.5 self-start sm:flex-row",children:[(0,n.jsxs)("a",{href:et,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2 text-[11px] font-medium text-zinc-300 hover:bg-white/[0.07]",children:["escrcpy",(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 opacity-70"})]}),(0,n.jsxs)("a",{href:ee,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2 text-[11px] font-medium text-zinc-300 hover:bg-white/[0.07]",children:["scrcpy",(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 opacity-70"})]})]})]}),a.length>0?(0,n.jsxs)("div",{className:"mt-5",children:[(0,n.jsxs)("p",{className:"mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:[(0,n.jsx)(O.Z,{className:"h-3.5 w-3.5"}),s("设备矩阵","Device matrix")]}),(0,n.jsx)("div",{className:"flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",children:a.map(e=>{let t="device"===e.state,l=r===e.serial,i=Q(e.transport),a=i.Icon;return(0,n.jsxs)("button",{type:"button",onClick:()=>o(e.serial),className:"flex min-w-[9rem] flex-col rounded-xl border px-3 py-2 text-left transition ".concat(l?"border-emerald-500/50 bg-emerald-500/15 ring-1 ring-emerald-400/30":"border-white/10 bg-black/30 hover:border-white/20"),children:[(0,n.jsxs)("span",{className:"flex items-center gap-1.5 text-xs font-medium text-zinc-200",children:[(0,n.jsx)(d.Z,{className:"h-3.5 w-3.5 text-emerald-400/80"}),(0,n.jsx)("span",{className:"truncate",children:e.model||e.serial.slice(0,10)})]}),(0,n.jsx)("span",{className:"mt-0.5 truncate font-mono text-[9px] text-zinc-600",children:e.serial}),(0,n.jsxs)("div",{className:"mt-1 flex flex-wrap gap-1",children:[(0,n.jsxs)("span",{className:"inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-[9px] font-semibold ring-1 ".concat(i.cls),children:[(0,n.jsx)(a,{className:"h-2.5 w-2.5"}),s(i.zh,i.en)]}),(0,n.jsx)("span",{className:"rounded-md px-1.5 py-0.5 text-[9px] font-semibold ".concat(t?"bg-emerald-500/20 text-emerald-200":"bg-zinc-600/30 text-zinc-500"),children:e.state})]})]},e.serial)})})]}):null,(0,n.jsxs)("div",{className:"mt-6 grid gap-6 xl:grid-cols-12",children:[(0,n.jsxs)("div",{className:"xl:col-span-8",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 border-b border-white/[0.06] pb-3",children:[(0,n.jsx)("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:s("刷新间隔","Interval")}),$.map(e=>(0,n.jsx)("button",{type:"button",onClick:()=>w(e),className:"rounded-lg px-2.5 py-1 text-[10px] font-semibold transition ".concat(v===e?"bg-violet-500/25 text-violet-100 ring-1 ring-violet-400/35":"bg-black/30 text-zinc-500 hover:text-zinc-300"),children:0===e?s("手动","Manual"):"".concat(e,"ms")},e)),(0,n.jsxs)("span",{className:"ml-1 inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[9px] font-medium ring-1 ".concat(eK.cls),children:[(0,n.jsx)(eF,{className:"h-3 w-3"}),"tcp"===eP?s("建议 ≥450ms","Suggest ≥450ms"):s("可试 280ms","Try 280ms")]}),(0,n.jsxs)("button",{type:"button",disabled:x,onClick:()=>void eT(),className:"ml-auto inline-flex items-center gap-1 rounded-lg border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] text-zinc-300 hover:bg-white/[0.08]",children:[(0,n.jsx)(y.Z,{className:"h-3 w-3 ".concat(N?"animate-spin":"")}),s("下一帧","Next frame")]}),(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>void h("wake"),className:"rounded-lg bg-amber-500/20 px-2.5 py-1 text-[10px] font-semibold text-amber-100 ring-1 ring-amber-500/30",children:"Wake"}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-1.5 text-[10px] text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/50",checked:R,onChange:e=>P(e.target.checked)}),(0,n.jsx)(H.Z,{className:"h-3 w-3"}),s("点按穿透","Tap-through")]}),(0,n.jsxs)("button",{type:"button",disabled:x||!r||!(null==eb?void 0:eb.server_jar),title:eb&&!eb.server_jar&&eb.hint_zh?eb.hint_zh:s("官方 scrcpy-server + WebSocket H.264","Official scrcpy-server + WebSocket H.264"),onClick:()=>{ec(e=>{let t=!e;return t?(ex(null),p(s("已开启实时画面(H.264 WebSocket)","Live H.264 WebSocket stream on"))):(r&&ex(r),p(s("已关闭实时画面(将使用截图)","Live stream off — screenshot mode"))),t})},className:"inline-flex items-center gap-1 rounded-lg border px-2.5 py-1 text-[10px] font-semibold transition ".concat(eo?"border-cyan-400/50 bg-cyan-500/20 text-cyan-100 ring-1 ring-cyan-400/35":"border-white/10 bg-black/30 text-zinc-400 hover:text-zinc-200"),children:[(0,n.jsx)(B.Z,{className:"h-3 w-3"}),eo?s("关闭真流","Stop WS"):s("实时画面","Live H.264")]})]}),eb&&!eb.server_jar?(0,n.jsxs)("p",{className:"mt-2 text-[10px] text-amber-200/80",children:[s("未检测到 scrcpy-server:启动时会自动拉取;内网可在 runtime.env 配置 SCRCPY_SERVER_DOWNLOAD_URL(支持 {tag})。","scrcpy-server missing: auto-fetched on startup; set SCRCPY_SERVER_DOWNLOAD_URL ({tag}) for mirrors."),eb.server_version?" (".concat(s("期望版本","version")," ").concat(eb.server_version,")"):null]}):null,(0,n.jsx)("p",{className:"mt-2 text-[10px] text-zinc-600",children:s("快捷键(镜像区聚焦时):H Home \xb7 B Back \xb7 R 多任务 \xb7 W 唤醒 \xb7 M 菜单","Hotkeys (mirror focused): H Home \xb7 B Back \xb7 R Recents \xb7 W Wake \xb7 M Menu")}),(0,n.jsxs)("div",{className:"relative mx-auto mt-4 max-h-[min(78vh,720px)] min-h-[280px]",children:[r?(0,n.jsxs)("div",{ref:ez,tabIndex:0,onKeyDown:e=>{if(!r||x)return;let t=e.target.tagName;if("INPUT"===t||"TEXTAREA"===t||"SELECT"===t)return;let s={h:["key",{keycode:"3"}],b:["key",{keycode:"4"}],r:["key",{keycode:"187"}],w:["wake",void 0],m:["key",{keycode:"82"}]}[e.key.toLowerCase()];s&&(e.preventDefault(),(async()=>{await h(s[0],s[1]),eo||eT()})())},className:"relative flex justify-center outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50",children:[(0,n.jsxs)("div",{className:"relative rounded-[2rem] border-[10px] border-zinc-800 bg-black shadow-inner shadow-black/80 ring-1 ring-white/10",style:{maxHeight:"min(72vh, 680px)"},children:[(0,n.jsx)("div",{className:"absolute left-1/2 top-0 z-10 h-5 w-24 -translate-x-1/2 rounded-b-xl bg-black/80"}),eo?(0,n.jsx)("canvas",{ref:eN,className:"max-h-[min(72vh,680px)] w-auto max-w-full rounded-[1.35rem] object-contain ".concat(R?"cursor-crosshair":"cursor-default"),draggable:!1,onClick:eH,onContextMenu:eB}):(0,n.jsx)("img",{ref:ew,src:eM||void 0,alt:"device",className:"max-h-[min(72vh,680px)] w-auto max-w-full rounded-[1.35rem] object-contain ".concat(R?"cursor-crosshair":"cursor-default"),draggable:!1,onLoadStart:()=>{j(!0)},onLoad:()=>{j(!1),eS.current=0,z(null)},onError:()=>{j(!1),eS.current+=1,eS.current<=3?(z(null),window.setTimeout(()=>eT(),380+200*eS.current)):z(s("截图连续失败:检查 USB 授权 / 无线是否断开 / Redroid 端口","Screenshot failed repeatedly"))},onClick:eZ,onContextMenu:eO})]}),eu?(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-cyan-500/90 px-2 py-0.5 text-[9px] font-bold text-cyan-950 shadow-lg",children:[(0,n.jsx)(B.Z,{className:"h-3 w-3"}),"SCRCPY\xb7WS"]}):v>0&&!eo?(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-emerald-500/90 px-2 py-0.5 text-[9px] font-bold text-emerald-950 shadow-lg",children:[(0,n.jsx)(K.Z,{className:"h-3 w-3"}),"LIVE"]}):eo?null:(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-zinc-700/90 px-2 py-0.5 text-[9px] font-bold text-zinc-200",children:[(0,n.jsx)(F.Z,{className:"h-3 w-3"}),s("单帧","Still")]})]}):(0,n.jsx)("p",{className:"py-16 text-center text-sm text-zinc-600",children:s("请选择或连接设备","Pick or connect a device")}),eh?(0,n.jsx)("p",{className:"mt-3 text-center text-xs text-rose-300",children:eh}):null,k&&!eo?(0,n.jsx)("p",{className:"mt-3 text-center text-xs text-rose-300",children:k}):null,(0,n.jsx)("p",{className:"mt-3 text-center text-[10px] text-zinc-600",children:s("左键单击 / 双击 \xb7 右键长按 \xb7 坐标对齐 PNG 像素","Left tap/double \xb7 right long-press \xb7 PNG pixel coords")})]})]}),(0,n.jsxs)("div",{className:"space-y-4 xl:col-span-4",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-black/40 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:s("遥测","Telemetry")}),u?(0,n.jsxs)("dl",{className:"mt-3 space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"device"}),(0,n.jsxs)("dd",{className:"text-right text-zinc-200",children:[u.brand," ",u.model]})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"Android"}),(0,n.jsxs)("dd",{className:"text-zinc-300",children:[u.android_version," (API ",u.sdk,")"]})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"wm size"}),(0,n.jsx)("dd",{className:"text-cyan-300/90",children:S?"".concat(S.width,"\xd7").concat(S.height):u.resolution||"—"})]}),(0,n.jsx)("div",{className:"whitespace-pre-wrap text-[10px] leading-snug text-zinc-500",children:u.battery})]}):(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-600",children:s("在上方点「刷新设备信息」","Use Refresh device info above")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/20 bg-gradient-to-b from-violet-500/[0.08] to-black/40 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-violet-300/80",children:s("导航坞","Nav dock")}),(0,n.jsx)("div",{className:"mt-3 grid grid-cols-3 gap-2",children:[["3","Home"],["4","Back"],["187","Recents"]].map(e=>{let[t,s]=e;return(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:t}),eT()},className:"rounded-xl border border-white/10 bg-white/[0.06] py-3 text-xs font-semibold text-zinc-200 hover:bg-white/[0.1]",children:s},t)})}),(0,n.jsxs)("div",{className:"mt-2 grid grid-cols-2 gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:"24"}),eT()},className:"rounded-xl border border-white/10 bg-black/30 py-2 text-[11px] text-zinc-300",children:"Vol+"}),(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:"25"}),eT()},className:"rounded-xl border border-white/10 bg-black/30 py-2 text-[11px] text-zinc-300",children:"Vol−"})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-cyan-500/[0.05] p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-cyan-300/80",children:s("手势","Gestures")}),(0,n.jsx)("div",{className:"mt-2 grid grid-cols-2 gap-2",children:["up","down","left","right"].map(e=>(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("swipe",{direction:e}),eT()},className:"rounded-lg border border-white/10 bg-black/30 py-2 font-mono text-[10px] uppercase text-zinc-300",children:e},e))})]})]})]})]})]})]})}let el=[{label:"getprop",cmd:"getprop ro.build.version.release"},{label:"top pkg",cmd:"dumpsys activity activities | grep -E 'mResumedActivity' | tail -n 1"},{label:"usb",cmd:"dumpsys usb | head -n 60"},{label:"surface",cmd:"dumpsys SurfaceFlinger | head -n 40"}];function ei(e,t,s){return e?(0,n.jsx)("span",{className:"rounded bg-emerald-500/15 px-2 py-0.5 text-[10px] text-emerald-200",children:t}):(0,n.jsx)("span",{className:"rounded bg-zinc-500/15 px-2 py-0.5 text-[10px] text-zinc-400",children:s})}function er(e){var t,s,l,i,r,a,o,c,d,x,u,m,h,p,b;let{t:g,notify:f,fetchJson:y,apiBase:w,busy:N,androidSerial:j,setAndroidSerial:k,devices:z,adbAvailable:S,apkPath:C,setApkPath:R,onAndroidAction:P,onCopy:I,onRefreshDevices:D}=e,[A,Z]=(0,_.useState)(null),[O,H]=(0,_.useState)(null),[B,K]=(0,_.useState)(null),[F,W]=(0,_.useState)(null),[Y,J]=(0,_.useState)(""),[G,V]=(0,_.useState)([]),[q,X]=(0,_.useState)("getprop ro.product.model"),[$,Q]=(0,_.useState)(""),[ee,et]=(0,_.useState)("300"),[es,er]=(0,_.useState)(""),[ea,eo]=(0,_.useState)(null),[ec,ed]=(0,_.useState)([]),[ex,eu]=(0,_.useState)(null),[em,eh]=(0,_.useState)("https://"),[ep,eb]=(0,_.useState)(""),[eg,ef]=(0,_.useState)(!1),[ev,ey]=(0,_.useState)(null),ew=!!(N||ev),eN=z.find(e=>e.serial===j)||null,ej=(0,_.useCallback)(async()=>{if(!j||!S){Z(null),H(null);return}ey("overview"),H(null);try{let e=await y("/android/overview?serial=".concat(encodeURIComponent(j)));Z(e)}catch(e){Z(null),H(e.message)}finally{ey(null)}},[S,j,y]),ek=(0,_.useCallback)(async()=>{W(null);try{let e=await y("/redroid/summary");K(e)}catch(e){K(null),W(e.message)}},[y]);(0,_.useEffect)(()=>{ej()},[ej]),(0,_.useEffect)(()=>{ek()},[ek]);let ez=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}ey("packages");try{let e=encodeURIComponent(Y.trim()),t=await y("/android/packages?serial=".concat(encodeURIComponent(j),"&query=").concat(e,"&limit=80"));V(t.packages||[])}catch(e){f(e.message),V([])}finally{ey(null)}},eS=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}let e=q.trim();if(e){ey("shell");try{var t,s;let n=await y("/android/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"shell",serial:j,payload:{command:e}})});Q(null!==(s=null!==(t=n.output)&&void 0!==t?t:n.message)&&void 0!==s?s:"")}catch(e){Q(e.message)}finally{ey(null)}}},e_=(0,_.useCallback)(async e=>{if(!j)return;let t=Math.min(3e3,Math.max(1,parseInt(ee,10)||200)),s=!!(null==e?void 0:e.silent);s||ey("logcat"),eo(null);try{var n;let e=await y("/android/logcat?serial=".concat(encodeURIComponent(j),"&lines=").concat(t));er(null!==(n=e.text)&&void 0!==n?n:"")}catch(e){er(""),eo(e.message)}finally{s||ey(null)}},[j,y,ee]);(0,_.useEffect)(()=>{if(!eg||!j||!S)return;let e=window.setInterval(()=>void e_({silent:!0}),3e3);return e_({silent:!0}),()=>clearInterval(e)},[S,j,eg,e_]);let eC=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}ey("ui"),eu(null);try{let e=await y("/android/ui?serial=".concat(encodeURIComponent(j),"&limit=100"));ed(e.nodes||[])}catch(e){ed([]),eu(e.message)}finally{ey(null)}},eR=async(e,t)=>{ey("redroid:".concat(e));try{let s=await y("/redroid/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,name:t||""})});s.summary&&K(s.summary),f(s.message||"OK"),await ek(),D()}catch(e){f(e.message)}finally{ey(null)}},eP=async e=>{await P("key",{keycode:e})},eL=async()=>{let e=ep.trim();if(!e){f(g("请输入文字","Enter text"));return}await P("text",{text:e})},eM=async()=>{let e=em.trim();if(!e||"https://"===e){f(g("请输入 URL","Enter URL"));return}await P("open_url",{url:e})};return(0,n.jsxs)("div",{className:"mx-auto max-w-6xl space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("安卓设备工作台","Android workstation")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:g("USB 真机与 Redroid 共用一套控制面,优先保证 USB 真机稳定。","USB phone and Redroid share one control surface.")})]}),S?(0,n.jsxs)("div",{className:"flex gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew||!j,onClick:()=>void ej(),className:"rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300",children:g("刷新设备","Refresh device")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>{ek(),D()},className:"rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300",children:g("刷新 Redroid","Refresh Redroid")})]}):(0,n.jsx)("span",{className:"text-xs text-amber-200/90",children:g("宿主机未检测到 adb","adb not found on host")})]}),(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:z.map(e=>(0,n.jsxs)("button",{type:"button",onClick:()=>k(e.serial),className:"rounded-xl border px-4 py-2 text-left text-xs ".concat(j===e.serial?"border-violet-500/50 bg-violet-500/10 text-white":"border-white/10 bg-black/30 text-zinc-400"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2",children:[(0,n.jsx)("span",{className:"font-medium text-zinc-200",children:e.model||e.serial}),e.transport?(0,n.jsx)("span",{className:"rounded bg-white/10 px-1.5 py-0.5 font-mono text-[9px] uppercase text-zinc-500",children:e.transport}):null,e.kind?(0,n.jsx)("span",{className:"rounded bg-cyan-500/10 px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-200",children:e.kind}):null]}),(0,n.jsx)("div",{className:"text-[10px] text-zinc-600",children:e.serial}),"device"!==(e.state||"").toLowerCase()?(0,n.jsx)("div",{className:"mt-1 text-[10px] font-medium text-amber-200/90",children:"unauthorized"===e.state?g("未授权,请在手机上允许 USB 调试。","Unauthorized, allow USB debugging on device."):"".concat(g("状态","State"),": ").concat(e.state)}):null]},e.serial))}),S&&0===z.length?(0,n.jsx)("p",{className:"mt-4 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3 text-xs leading-relaxed text-amber-200/90",children:g("当前没有已授权设备。USB 真机请检查调试授权;Redroid 请先启动实例,再 adb connect 127.0.0.1:5555。","No authorized devices. For USB phones, confirm USB debugging. For Redroid, start an instance then adb connect 127.0.0.1:5555.")}):null]}),(0,n.jsxs)("div",{className:"grid gap-4 xl:grid-cols-[1.1fr_0.9fr]",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/15 bg-zinc-950/50 p-5",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-violet-300/90",children:g("真机诊断","Phone diagnostics")}),O?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:O}):null,A?(0,n.jsxs)("div",{className:"mt-3 space-y-3",children:[(0,n.jsxs)("dl",{className:"space-y-1.5 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"model"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[A.brand," ",A.model]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"android"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[A.android_version," (sdk ",A.sdk,")"]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"kind"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[A.kind||(null==eN?void 0:eN.kind)||"unknown"," / ",A.transport||(null==eN?void 0:eN.transport)||"unknown"]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"display"})," ",(0,n.jsx)("dd",{className:"inline text-zinc-300",children:A.resolution})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"rotation"})," ",(0,n.jsx)("dd",{className:"inline text-zinc-300",children:A.rotation})]}),(0,n.jsx)("div",{className:"whitespace-pre-wrap text-[10px] leading-snug text-zinc-500",children:A.battery}),(0,n.jsx)("div",{className:"line-clamp-3 text-[10px] text-zinc-500",children:A.focus})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[ei(null===(t=A.diagnostics)||void 0===t?void 0:t.ready,g("已启动","Boot ready"),g("未完成启动","Boot pending")),ei(null===(s=A.diagnostics)||void 0===s?void 0:s.google_play,"Google Play","No Play"),ei(null===(l=A.diagnostics)||void 0===l?void 0:l.google_services,"GMS","No GMS"),ei(null===(i=A.diagnostics)||void 0===i?void 0:i.magisk,"Magisk","No Magisk"),ei(null===(r=A.diagnostics)||void 0===r?void 0:r.root,"Root","No root")]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/5 bg-black/20 p-3 text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:["boot_completed: ",(null===(a=A.diagnostics)||void 0===a?void 0:a.boot_completed)||"-"]}),(0,n.jsxs)("div",{children:["provisioned: ",(null===(o=A.diagnostics)||void 0===o?void 0:o.device_provisioned)||"-"]}),(0,n.jsxs)("div",{children:["setup_complete: ",(null===(c=A.diagnostics)||void 0===c?void 0:c.user_setup_complete)||"-"]}),(0,n.jsxs)("div",{children:["abi: ",(null===(d=A.diagnostics)||void 0===d?void 0:d.abi)||"-"]}),(0,n.jsxs)("div",{className:"truncate",children:["hardware: ",(null===(x=A.diagnostics)||void 0===x?void 0:x.hardware)||"-"]})]})]}):O?null:(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:g("加载中","Loading")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-zinc-950/50 p-5",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-3",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-cyan-300/90",children:g("Redroid 板块","Redroid panel")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("start"),className:"rounded-lg bg-emerald-500/20 px-3 py-1.5 text-xs text-emerald-100 ring-1 ring-emerald-500/30",children:g("启动默认实例","Start default")})]}),F?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:F}):null,B?(0,n.jsxs)("div",{className:"mt-3 space-y-3 text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsxs)("div",{children:["arch: ",B.arch||"-"]}),(0,n.jsxs)("div",{className:"truncate",children:["image: ",B.image||"-"]}),(0,n.jsxs)("div",{children:["profile: ",B.device_profile||"-"]}),(0,n.jsxs)("div",{children:["gpu: ",B.gpu_mode||"-"]}),(0,n.jsxs)("div",{children:["adb: 127.0.0.1:",B.default_port||5555]})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[ei(null===(u=B.requirements)||void 0===u?void 0:u.docker_available,"Docker","No Docker"),ei(null===(m=B.requirements)||void 0===m?void 0:m.adb_available,"ADB","No ADB"),ei(null===(h=B.requirements)||void 0===h?void 0:h.kvm,"KVM","No KVM"),ei(null===(p=B.requirements)||void 0===p?void 0:p.binderfs,"binderfs","No binderfs")]}),(0,n.jsxs)("div",{className:"space-y-2",children:[(B.instances||[]).map(e=>{var t,s,l,i;return(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-3",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"font-medium text-zinc-100",children:e.name}),(0,n.jsxs)("div",{className:"font-mono text-[10px] text-zinc-500",children:[e.port?"127.0.0.1:".concat(e.port):"-"," / ",e.status_text||"-"]})]}),(0,n.jsxs)("div",{className:"flex gap-1",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("start",e.name),className:"rounded bg-emerald-500/15 px-2 py-1 text-[10px] text-emerald-200",children:"Start"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("restart",e.name),className:"rounded bg-amber-500/15 px-2 py-1 text-[10px] text-amber-200",children:"Restart"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("adb_connect",e.name),className:"rounded bg-cyan-500/15 px-2 py-1 text-[10px] text-cyan-200",children:"ADB"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("stop",e.name),className:"rounded bg-rose-500/15 px-2 py-1 text-[10px] text-rose-200",children:"Stop"})]})]}),(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap gap-2",children:[ei(null===(t=e.diagnostics)||void 0===t?void 0:t.ready,g("系统就绪","System ready"),g("启动中","Booting")),ei(null===(s=e.diagnostics)||void 0===s?void 0:s.google_play,"Play","No Play"),ei(null===(l=e.diagnostics)||void 0===l?void 0:l.magisk,"Magisk","No Magisk"),ei(null===(i=e.diagnostics)||void 0===i?void 0:i.root,"Root","No root")]}),e.data_dir?(0,n.jsx)("div",{className:"mt-2 truncate font-mono text-[10px] text-zinc-500",children:e.data_dir}):null]},e.name)}),(null===(b=B.instances)||void 0===b?void 0:b.length)?null:(0,n.jsx)("p",{className:"rounded-xl border border-white/5 bg-black/20 p-3 text-xs text-zinc-500",children:g("当前没有 Redroid 实例。默认动作会创建 redroid13-1。","No Redroid instances yet. Default actions create redroid13-1.")})]})]}):(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:g("读取 Redroid 状态中","Loading Redroid status")})]})]}),(0,n.jsx)(en,{t:g,apiBase:w,fetchJson:y,serial:j,devices:z,setSerial:k,adbAvailable:S,lock:ew,overview:A,onAndroidAction:P,notify:f,onRefreshDevices:D}),S&&j?(0,n.jsxs)("div",{className:"grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-emerald-300/90",children:g("快捷控制","Quick controls")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("3"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Home"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("4"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Back"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("187"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Recents"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("wake"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Wake"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("launch_package",{package:"com.zhiliaoapp.musically"}),className:"rounded-lg bg-gradient-to-r from-pink-500/25 to-violet-500/25 px-3 py-1.5 text-xs ring-1 ring-white/10",children:"TikTok"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("24"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Vol+"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("25"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Vol-"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("66"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Enter"})]}),(0,n.jsx)("p",{className:"mt-3 text-[10px] text-zinc-600",children:g("滑动手势","Swipe gestures")}),(0,n.jsx)("div",{className:"mt-1 flex flex-wrap gap-2",children:["up","down","left","right"].map(e=>(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("swipe",{direction:e}),className:"rounded-lg border border-white/10 bg-black/30 px-2.5 py-1 font-mono text-[10px] uppercase text-zinc-300",children:e},e))}),(0,n.jsxs)("div",{className:"mt-4 rounded-lg border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:g("ADB 文本输入","ADB text input")}),(0,n.jsxs)("div",{className:"mt-2 flex flex-col gap-2 sm:flex-row",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-xs",value:ep,onChange:e=>eb(e.target.value),placeholder:"hello / 123",onKeyDown:e=>{"Enter"===e.key&&eL()}}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eL(),className:"rounded-lg bg-emerald-500/20 px-3 py-2 text-xs text-emerald-100 ring-1 ring-emerald-500/30",children:g("发送","Send")})]})]}),(0,n.jsxs)("div",{className:"mt-4 flex flex-col gap-2 sm:flex-row sm:items-center",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-xs",value:em,onChange:e=>eh(e.target.value),placeholder:"https://"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eM(),className:"rounded-lg bg-violet-500/20 px-3 py-2 text-xs text-violet-100 ring-1 ring-violet-500/30",children:g("打开链接","Open URL")})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsxs)("h4",{className:"flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-cyan-300/90",children:[(0,n.jsx)(L.Z,{className:"h-4 w-4"}),g("USB 真机优化建议","USB optimization")]}),(0,n.jsxs)("ul",{className:"mt-3 space-y-2 text-xs leading-relaxed text-zinc-400",children:[(0,n.jsx)("li",{children:g("优先使用主板直连 USB,不要经过不稳的 HUB。","Prefer direct motherboard USB, not unstable hubs.")}),(0,n.jsx)("li",{children:g("开发板端固定 adb server,避免频繁 kill-server。","Keep adb server stable and avoid frequent kill-server.")}),(0,n.jsx)("li",{children:g("镜像页默认支持 scrcpy H.264 真流,截图模式作为回退。","Use scrcpy H.264 live stream first, screenshot mode as fallback.")}),(0,n.jsx)("li",{children:g("如果 TikTok / YouTube App 直播,要先确认屏幕常亮、分辨率稳定、USB 供电足够。","For TikTok / YouTube app live, keep screen on, resolution stable, and USB power sufficient.")})]})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(M.Z,{className:"h-4 w-4 text-emerald-400"}),g("包名搜索与启动","Packages")]}),(0,n.jsxs)("div",{className:"mt-3 flex flex-col gap-2 sm:flex-row",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-xl border border-white/10 bg-black/40 px-4 py-2 text-sm",value:Y,onChange:e=>J(e.target.value),placeholder:"tiktok / musically / youtube",onKeyDown:e=>{"Enter"===e.key&&ez()}}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void ez(),className:"rounded-xl bg-emerald-500/20 px-4 py-2 text-sm text-emerald-100 ring-1 ring-emerald-500/30",children:g("搜索","Search")})]}),(0,n.jsxs)("ul",{className:"mt-4 max-h-48 space-y-1 overflow-y-auto rounded-lg border border-white/5 bg-black/30 p-2 font-mono text-[11px]",children:[G.map(e=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 py-1 text-zinc-400",children:[(0,n.jsx)("span",{className:"min-w-0 truncate text-zinc-300",children:e.package}),(0,n.jsxs)("span",{className:"flex shrink-0 gap-1",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("launch_package",{package:e.package}),className:"rounded bg-white/10 px-2 py-0.5 text-[10px] text-zinc-200",children:g("启动","Launch")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("force_stop",{package:e.package}),className:"rounded bg-rose-500/15 px-2 py-0.5 text-[10px] text-rose-200",children:g("停止","Stop")})]})]},e.package)),G.length?null:(0,n.jsx)("li",{className:"py-2 text-center text-zinc-600",children:g("暂无结果","No results")})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"grid gap-6 xl:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(T.Z,{className:"h-4 w-4 text-amber-400"}),"ADB shell"]}),(0,n.jsx)("div",{className:"mt-2 flex flex-wrap gap-1.5",children:el.map(e=>(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>X(e.cmd),className:"rounded-md border border-white/10 bg-white/[0.04] px-2 py-1 text-[10px] text-zinc-400 hover:text-zinc-200",children:e.label},e.label))}),(0,n.jsx)("textarea",{className:"mt-3 min-h-[100px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-3 font-mono text-xs text-zinc-200",value:q,onChange:e=>X(e.target.value),spellCheck:!1,onKeyDown:e=>{"Enter"===e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),eS())}}),(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eS(),className:"rounded-xl bg-amber-500/20 px-4 py-2 text-sm text-amber-100 ring-1 ring-amber-500/30",children:"shell"===ev?(0,n.jsxs)("span",{className:"inline-flex items-center gap-2",children:[(0,n.jsx)(v.Z,{className:"h-3.5 w-3.5 animate-spin"}),"Run"]}):"Run"}),$?(0,n.jsxs)("button",{type:"button",disabled:!$,onClick:()=>I($),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-200",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),g("复制输出","Copy")]}):null]}),$?(0,n.jsx)("pre",{className:"mt-3 max-h-64 overflow-auto whitespace-pre-wrap rounded-lg border border-white/5 bg-black/40 p-3 font-mono text-[11px] text-zinc-400 xl:max-h-[min(24rem,50vh)]",children:$}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("Logcat","Logcat")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap items-center gap-2",children:[(0,n.jsx)("input",{className:"w-24 rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-xs",value:ee,onChange:e=>et(e.target.value)}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2 text-[11px] text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:eg,onChange:e=>ef(e.target.checked)}),g("每 3 秒自动刷新","Auto every 3s")]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void e_(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:g("拉取日志","Pull logs")}),es?(0,n.jsxs)("button",{type:"button",onClick:()=>I(es),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-200",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),g("复制","Copy")]}):null]}),ea?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:ea}):null,es?(0,n.jsx)("pre",{className:"mt-3 max-h-72 overflow-auto whitespace-pre-wrap rounded-lg border border-white/5 bg-black/50 p-3 font-mono text-[10px] leading-tight text-zinc-500 xl:max-h-[min(24rem,50vh)]",children:es}):null]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(E.Z,{className:"h-4 w-4 text-fuchsia-400"}),g("界面节点","UI nodes")]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eC(),className:"mt-3 rounded-xl border border-fuchsia-500/30 bg-fuchsia-500/10 px-4 py-2 text-sm text-fuchsia-100",children:g("刷新节点","Refresh nodes")}),ex?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:ex}):null,(0,n.jsxs)("ul",{className:"mt-3 max-h-64 space-y-1 overflow-y-auto rounded-lg border border-white/5 bg-black/30 p-2 text-[11px]",children:[ec.map((e,t)=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 border-b border-white/[0.04] py-1.5 text-zinc-400 last:border-0",children:[(0,n.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,n.jsx)("span",{className:"text-zinc-200",children:e.label}),e.resource_id?(0,n.jsx)("span",{className:"ml-2 font-mono text-[10px] text-zinc-600",children:e.resource_id}):null,(0,n.jsxs)("div",{className:"font-mono text-[10px] text-zinc-600",children:["(",e.center_x,", ",e.center_y,")",e.clickable?" click":""]})]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("tap",{x:e.center_x,y:e.center_y}),className:"shrink-0 rounded bg-white/10 px-2 py-1 text-[10px] text-zinc-200",children:"Tap"})]},"".concat(e.center_x,"-").concat(e.center_y,"-").concat(t))),ec.length||ex?null:(0,n.jsx)("li",{className:"py-4 text-center text-zinc-600",children:g("尚未加载","Not loaded")})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("安装 APK","Install APK")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:g("先 adb push 到 /sdcard/ 再执行安装。","Push the APK to /sdcard/ first.")}),(0,n.jsx)("input",{className:"mt-4 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm",value:C,onChange:e=>R(e.target.value)}),(0,n.jsx)("button",{type:"button",disabled:ew||!S||!j,onClick:()=>void P("install_apk",{path:C}),className:"mt-3 rounded-xl bg-emerald-500/20 px-4 py-2 text-sm font-medium text-emerald-100 ring-1 ring-emerald-500/30",children:"pm install"})]}):null]})}var ea=s(9897),eo=s(4554),ec=s(7689),ed=s(8930),ex=s(8401);function eu(e){let{t,title:s,subtitle:l,steps:i,onNavigate:r}=e;return(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/25 bg-gradient-to-br from-violet-500/10 via-zinc-950/40 to-fuchsia-500/5 p-6 shadow-lg shadow-violet-900/10",children:[(0,n.jsx)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:s}),(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-400",children:l}),(0,n.jsx)("div",{className:"mt-6 flex flex-col items-center gap-4 sm:flex-row sm:flex-wrap sm:justify-center",children:i.map((e,t)=>(0,n.jsxs)("div",{className:"flex items-center gap-4",children:[e.openUrl||e.target&&r?(0,n.jsxs)("button",{type:"button",onClick:()=>{if(e.openUrl){window.open((0,ex.p)(e.openUrl),"_blank","noopener,noreferrer");return}e.target&&r&&r(e.target)},className:"flex min-h-[4rem] min-w-[7rem] flex-col items-center justify-center rounded-2xl bg-gradient-to-br px-4 py-2 text-center text-xs font-bold text-white shadow-lg ring-2 ring-white/10 transition hover:ring-violet-400/50 hover:brightness-110 ".concat(e.gradient),children:[(0,n.jsx)("span",{children:e.label}),e.sub?(0,n.jsx)("span",{className:"mt-1 text-[10px] font-normal text-white/80",children:e.sub}):null]}):(0,n.jsxs)("div",{className:"flex min-h-[4rem] min-w-[7rem] flex-col items-center justify-center rounded-2xl bg-gradient-to-br px-4 py-2 text-center text-xs font-bold text-white shadow-lg ".concat(e.gradient),children:[(0,n.jsx)("span",{children:e.label}),e.sub?(0,n.jsx)("span",{className:"mt-1 text-[10px] font-normal text-white/80",children:e.sub}):null]}),t8?e.slice(-8):e;if(e.includes("live2/")){let t=e.split("/"),s=t[t.length-1]||"";return s.length>8?s.slice(-8):s}}}let t=e.trim();return!t||t.includes("\n")||t.toLowerCase().startsWith("rtmp")?"":t.length>8?t.slice(-8):t}function eh(e){let t=[];for(let s of e.split(/\r?\n/)){let e=s.trim();if(!e||e.startsWith("#")||!/douyin\.com|v\.douyin\.com/i.test(e))continue;let n=e.match(/(https?:\/\/live\.douyin\.com\/[a-zA-Z0-9_-]+)/i);if(n){t.push(n[1].replace(/^http:\/\//i,"https://"));continue}let l=e.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);if(l){t.push("https://live.douyin.com/".concat(l[1]));continue}let i=e.match(/(\d{8,})/);i&&t.push("https://live.douyin.com/".concat(i[1]))}return Array.from(new Set(t)).slice(0,16)}function ep(e){let t="",s=!0,n="",l=!1,i=e.split(/\r?\n/),r=!1;for(let e of i){let i=e.trim();if(/^\[youtube\]/i.test(i)){r=!0;continue}if(/^\[/.test(i)){r=!1;continue}if(!r||!i||i.startsWith("#")||i.startsWith(";"))continue;let a=i.indexOf("=");if(a<0)continue;let o=i.slice(0,a).trim().toLowerCase(),c=i.slice(a+1).trim().split(/[#;]/)[0].trim();"key"===o||"stream_key"===o?t=c:"rtmps"===o?s=!/^(0|false|否)/i.test(c):"bitrate"===o?n=c:"fast_audio"===o&&(l=/^(1|true|yes|是|开|on)/i.test(c))}return{key:t,rtmps:s,bitrate:n,fastAudio:l}}function eb(e){return"URL_config.".concat(e.replace(/[^a-zA-Z0-9_.-]/g,"_"),".ini")}function eg(e){let t=e.bitrate.trim()?"bitrate = ".concat(e.bitrate.trim()):"# bitrate = 4000";return"[youtube]\n# YouTube 推流 key(工作室 → 直播 → 串流密钥)\nkey = ".concat(e.key.trim(),"\n\n# 使用 RTMPS;填 否 则 RTMP\nrtmps = ").concat(e.rtmps?"是":"否","\n\n# 视频比特率 kbps(可选)\n").concat(t,"\n\n# 轻量音频链,减轻 CPU:1 / 是\nfast_audio = ").concat(e.fastAudio?"1":"0","\n")}function ef(e){if(e<60)return"".concat(e,"s");let t=Math.floor(e/60);return t<60?"".concat(t,"m ").concat(e%60,"s"):"".concat(Math.floor(t/60),"h ").concat(t%60,"m")}function ev(e){var t,s,l;let{t:i,currentProc:r,fetchJson:a,urlListText:o,keySuffixUi:c,showKeyRow:d=!0,pollMs:x=1e3,refreshToken:u=0}=e,[m,h]=(0,_.useState)(""),[p,b]=(0,_.useState)(""),[g,f]=(0,_.useState)(""),[v,w]=(0,_.useState)(""),[N,j]=(0,_.useState)(""),[k,z]=(0,_.useState)([]),[S,C]=(0,_.useState)(null),[R,P]=(0,_.useState)(!1),[L,M]=(0,_.useState)(null),[T,U]=(0,_.useState)(0),[E,I]=(0,_.useState)(!0),D=(0,_.useRef)(null),A=(0,_.useRef)(null),O=(0,_.useRef)(!1),H=(0,_.useRef)(!1),B=(0,_.useRef)(!1),W=(0,_.useRef)(r),Y=(0,_.useRef)(0),J=function(e){let t=[];for(let s of e.split(/\r?\n/)){let e=s.trim();if(!e||e.startsWith("#")||!/douyin\.com|v\.douyin\.com/i.test(e))continue;let n=e.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);if(n){t.push(n[1]);continue}let l=e.match(/(\d{8,})/);t.push(l?l[1]:e.slice(0,96))}return Array.from(new Set(t)).slice(0,12)}(o),G=/^(online|running)$/i.test(m),V=function(e){if(!e)return null;let t=null;for(let s of e.split(/\r?\n/)){let e=s.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);e&&(t=e[1]);let n=s.match(/https?:\/\/[^/\s]*douyin\.com\/(\d{8,})/i);n&&(t=n[1])}return t}(p),q=null!==(s=eh(o)[0])&&void 0!==s?s:"",X=V||(null!==(l=null===(t=q.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i))||void 0===t?void 0:t[1])&&void 0!==l?l:null),$=X?"https://live.douyin.com/".concat(X):q||null,Q=(0,_.useCallback)(async()=>{let e=W.current.trim();if(!e)return;if(H.current){B.current=!0;return}H.current=!0;let t=++Y.current;try{let s=await a("/status?process=".concat(encodeURIComponent(e)));if(t!==Y.current||e!==W.current.trim())return;let n=s.process_status||"unknown";h(n),b(s.recent_log||""),f(s.recent_error||""),w(s.business_status||""),j(s.business_note||""),z(Array.isArray(s.ffmpeg_pids)?s.ffmpeg_pids:[]),C("number"==typeof s.log_age_seconds?s.log_age_seconds:null);let l=/^(online|running)$/i.test(n);l&&!O.current&&M(Date.now()),l||M(null),O.current=l}catch(s){if(t!==Y.current||e!==W.current.trim())return;h("error"),w("error"),j(""),z([]),C(null)}finally{H.current=!1,B.current&&(B.current=!1,window.setTimeout(()=>void Q(),0))}},[a]);(0,_.useEffect)(()=>{if(W.current=r,Y.current+=1,B.current=!1,!r){h(""),b(""),f(""),w(""),j(""),z([]),C(null),M(null),O.current=!1;return}h(""),b(""),f(""),w(""),j(""),z([]),C(null),M(null),O.current=!1,Q()},[r,Q]),(0,_.useEffect)(()=>{let e=()=>I("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{r&&Q()},[r,Q,u]),(0,_.useEffect)(()=>{if(R||!E||!r)return;let e=window.setInterval(()=>void Q(),x);return()=>clearInterval(e)},[R,E,r,Q,x]),(0,_.useEffect)(()=>{let e=()=>{"visible"===document.visibilityState&&Q()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[Q]),(0,_.useEffect)(()=>{if(!L)return;let e=window.setInterval(()=>U(e=>e+1),1e3);return()=>clearInterval(e)},[L]),(0,_.useEffect)(()=>{if(R||!E)return;let e=D.current;e&&(e.scrollTop=e.scrollHeight)},[p,R,E]),(0,_.useEffect)(()=>{if(R||!E)return;let e=A.current;e&&(e.scrollTop=e.scrollHeight)},[g,R,E]);let ee=null==L?i("—(未检测到在线)","— (not online)"):ef(Math.max(0,Math.floor((Date.now()-L)/1e3))),et=R?i("已暂停刷新","Paused"):E?i("每秒刷新","~1s refresh"):i("后台暂停","Hidden"),es=R?"bg-amber-500/20 text-amber-100 ring-amber-500/35":E?"bg-emerald-500/15 text-emerald-200/90 ring-emerald-500/25":"bg-sky-500/15 text-sky-100 ring-sky-500/25";return(0,n.jsxs)("section",{className:"rounded-2xl border border-emerald-500/20 bg-gradient-to-b from-emerald-500/[0.07] to-zinc-950/80 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("span",{className:"sr-only","aria-hidden":!0,children:T}),(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:i("当前直播日志","Live process log")}),(0,n.jsx)("span",{className:"rounded-full bg-white/10 px-2 py-0.5 font-mono text-[10px] text-zinc-300 ring-1 ring-white/10",children:r||"—"}),(0,n.jsx)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ".concat(es),children:et})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsxs)("button",{type:"button",onClick:()=>void Q(),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300 hover:bg-white/[0.08]",children:[(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5"}),i("立即拉取","Refresh")]}),(0,n.jsxs)("button",{type:"button",onClick:()=>P(e=>!e),className:"inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-xs ring-1 ".concat(R?"bg-amber-500/20 text-amber-100 ring-amber-500/35":"border border-white/10 bg-black/30 text-zinc-300"),children:[R?(0,n.jsx)(K.Z,{className:"h-3.5 w-3.5"}):(0,n.jsx)(F.Z,{className:"h-3.5 w-3.5"}),R?i("继续自动刷新","Resume"):i("暂停自动刷新","Pause")]})]})]}),(0,n.jsxs)("div",{className:"mt-4 space-y-3 rounded-xl border border-white/[0.06] bg-black/25 p-4",children:[(0,n.jsxs)("div",{className:"grid gap-2 text-xs sm:grid-cols-2",children:[(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("进程状态","Status")}),(0,n.jsx)("span",{className:"font-medium text-zinc-200",children:function(e,t){let s=(t||"unknown").toLowerCase();return"online"===s||"running"===s?e("运行中(PM2/进程在线)","Running (process online)"):"stopped"===s||"stopping"===s?e("已停止","Stopped"):"not_found"===s||"errored"===s?e("未运行或未找到","Not running / not found"):"invalid"===s?e("无效进程名","Invalid process"):t||"—"}(i,m)})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("Business state","Business state")}),(0,n.jsx)("span",{className:"font-medium text-cyan-200/90",children:function(e,t,s){let n=(t||"").toLowerCase();return"streaming"===n?e("Pushing to YouTube","Pushing to YouTube"):"waiting_source"===n?e("Waiting for the upstream live source","Waiting for the upstream live source"):"source_live"===n?e("Source live detected, but no active FFmpeg relay worker","Source live detected, but no active FFmpeg relay worker"):"monitoring"===n?e("Process online and monitoring the source","Process online and monitoring the source"):"misconfigured"===n?e("YouTube key missing or invalid","YouTube key missing or invalid"):"stale"===n?e("Process online but the log heartbeat is stale","Process online but the log heartbeat is stale"):"error"===n?e("Recent process error detected","Recent process error detected"):(null==s?void 0:s.trim())?s.trim():e("No business-state hint yet","No business-state hint yet")}(i,v,N)})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("本次在线时长(估算)","Online duration (est.)")}),(0,n.jsx)("span",{className:"font-mono text-cyan-200/90",children:ee})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("FFmpeg workers","FFmpeg workers")}),(0,n.jsxs)("span",{className:"font-mono text-zinc-300",children:[k.length?k.join(", "):"none",null!=S?" \xb7 log ".concat(ef(Math.max(0,Math.floor(S)))):""]})]})]}),d?(0,n.jsxs)("div",{className:"flex flex-wrap gap-2 text-xs",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("YouTube key 尾码(参考)","YT key suffix")}),(0,n.jsx)("span",{className:"font-mono text-zinc-300",children:c||"—"})]}):null,$?(0,n.jsxs)("div",{className:"rounded-lg border border-violet-500/20 bg-violet-500/[0.06] px-3 py-2.5",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsx)("span",{className:"text-[11px] font-medium text-zinc-400",children:V?i("当前直播源站(日志)","Current source (from log)"):G?i("配置中的源站地址","Configured source URL"):i("源站快捷入口","Source quick link")}),(0,n.jsxs)("a",{href:$,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs font-medium text-violet-100 ring-1 ring-violet-500/35 transition hover:bg-violet-500/30",children:[(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 shrink-0 opacity-90"}),X||i("打开抖音直播页","Open Douyin live")]})]}),J.length>1&&V?(0,n.jsxs)("p",{className:"mt-2 text-[10px] text-zinc-600",children:[i("配置中还有","Also in config:")," ",J.filter(e=>e!==X).slice(0,3).map(e=>(0,n.jsx)("a",{href:"https://live.douyin.com/".concat(e),target:"_blank",rel:"noreferrer",className:"ml-1 text-violet-400/90 underline-offset-2 hover:underline",children:e},e)),J.length>4?"…":null]}):null]}):(0,n.jsx)("p",{className:"text-[11px] text-zinc-600",children:i("(日志与地址列表中暂无抖音房间)","(No Douyin room in log or URL list)")})]}),"not_found"!==m&&"invalid"!==m?(0,n.jsxs)("div",{className:"mt-4 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"text-[11px] font-semibold uppercase tracking-wider text-emerald-400/90",children:i("进程输出(stdout / 合并日志)","Process output")}),(0,n.jsx)("pre",{ref:D,className:"log-live-scroll mt-2 max-h-[min(22rem,42vh)] min-h-[12rem] overflow-auto whitespace-pre-wrap rounded-xl border border-emerald-500/20 bg-black/55 p-3 font-mono text-[10px] leading-relaxed text-emerald-100/95",children:p.trim()||i("(暂无)","(empty)")})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"text-[11px] font-semibold uppercase tracking-wider text-rose-400/90",children:i("异常信息(stderr)","Stderr")}),(0,n.jsx)("pre",{ref:A,className:"log-live-scroll mt-2 max-h-[min(22rem,42vh)] min-h-[12rem] overflow-auto whitespace-pre-wrap rounded-xl border border-rose-500/20 bg-black/55 p-3 font-mono text-[10px] leading-relaxed text-rose-100/95",children:g.trim()||i("(暂无)","(empty)")})]})]}):(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:i("进程未在 PM2 / 本地注册表中运行,日志区域已隐藏。","Process not running — log panes hidden.")})]})}function ey(e){return/tiktok/i.test(e.script)||/tiktok/i.test(e.pm2)}let ew=[{id:"720p60",labelZh:"720p @ 60Hz",labelEn:"720p @ 60Hz",spec:"1280x720, 59.94/60Hz, RGB/YUV 有限",hintZh:"板端 HDMI 输出时序与显示端一致;下游画布建议 1280\xd7720。",hintEn:"Match SoC HDMI timing to the display; downstream canvas e.g. 1280\xd7720."},{id:"1080p30",labelZh:"1080p @ 30Hz",labelEn:"1080p @ 30Hz",spec:"1920x1080, 30Hz",hintZh:"带宽紧张时可优先 1080p30;检查线材与接口规格。",hintEn:"Prefer 1080p30 when bandwidth is tight; check cable and port specs."},{id:"1080p60",labelZh:"1080p @ 60Hz",labelEn:"1080p @ 60Hz",spec:"1920x1080, 60Hz, 高刷转播",hintZh:"两端均需稳定支持 1080p60;长时间运行注意散热。",hintEn:"Both ends need stable 1080p60; mind thermals for long runs."}],eN="live-hub-tiktok-hdmi-check-v1",ej="live-hub-tiktok-hdmi-notes-v1",ek="live-hub-tiktok-source-mode-v1";function ez(e){var t;let{t:s,busy:l,entries:i,currentProc:r,setCurrentProc:a,urlConfig:o,setUrlConfig:c,onRunProcess:d,onSaveUrlConfig:x,onReplayGoLive:u,onCopy:m,fetchJson:h,apiPrefix:p,quickLinks:b,hdmiStatus:g,notify:f,onNavigate:v}=e,y=(0,_.useMemo)(()=>i.filter(ey),[i]),w=y.length?y:i,[N,j]=(0,_.useState)("srs"),[k,z]=(0,_.useState)([]),[S,C]=(0,_.useState)(!1),[R,P]=(0,_.useState)(!1),[L,M]=(0,_.useState)("1080p30"),[T,E]=(0,_.useState)("1080p30"),[I,A]=(0,_.useState)(!1),[Z,O]=(0,_.useState)(!0),[H,K]=(0,_.useState)(null),F=(0,_.useRef)(!1),W=(0,_.useRef)(o),[J,G]=(0,_.useState)(""),[V,q]=(0,_.useState)({}),X=(0,_.useCallback)(e=>{W.current=e,c(e)},[c]),$=(0,_.useCallback)(e=>{j(e);try{window.localStorage.setItem(ek,e)}catch(e){}},[]);(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(ek);("ffmpeg"===e||"replay"===e||"srs"===e)&&("ffmpeg"!==e&&"replay"!==e?j(e):j("srs"))}catch(e){}},[]),(0,_.useEffect)(()=>{F.current=!1},[r]),(0,_.useEffect)(()=>{W.current=o},[o]);let Q=(0,_.useCallback)(async()=>{if(r)try{var e;let t=new URLSearchParams({process:r,_ts:String(Date.now())}),s=await h("/get_url_config?".concat(t));X(null!==(e=s.content)&&void 0!==e?e:"")}catch(e){}},[X,r,h]);(0,_.useEffect)(()=>{if(!r||!Z||"ffmpeg"!==N)return;let e=window.setInterval(()=>{F.current||Q()},8e3);return()=>clearInterval(e)},[r,Z,Q,N]),(0,_.useEffect)(()=>{"ffmpeg"===N&&r&&Q()},[N,r,Q]);let ee=(0,_.useCallback)(async()=>{if(r){C(!0);try{var e;let t=await h("/tiktok_replay/list?process=".concat(encodeURIComponent(r)));z(null!==(e=t.files)&&void 0!==e?e:[])}catch(e){f(e.message),z([])}finally{C(!1)}}},[r,h,f]);(0,_.useEffect)(()=>{"replay"===N&&r&&ee()},[N,r,ee]),(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(eN);e&&q(JSON.parse(e))}catch(e){}try{G(window.localStorage.getItem(ej)||"")}catch(e){}},[]);let et=(0,_.useCallback)(e=>{q(e);try{window.localStorage.setItem(eN,JSON.stringify(e))}catch(e){}},[]),es=e=>{G(e);try{window.localStorage.setItem(ej,e)}catch(e){}},en=async()=>{if(r){A(!0),K(null),F.current=!1;try{var e;let t=new URLSearchParams({process:r,_ts:String(Date.now())}),n=await h("/get_url_config?".concat(t));X(null!==(e=n.content)&&void 0!==e?e:""),K(s("已从服务器读取 URL_config","Reloaded URL_config from server"))}catch(e){K(e.message)}finally{A(!1)}}},el=async()=>{try{await Promise.resolve(x(W.current)),F.current=!1}catch(e){}},ei=async e=>{if(r){P(!0);try{await h("/tiktok_replay/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({process:r,filename:e})}),f(s("已删除文件","File removed")),await ee()}catch(e){f(e.message)}finally{P(!1)}}},er=async e=>{var t;let n=null===(t=e.target.files)||void 0===t?void 0:t[0];if(e.target.value="",n&&r){P(!0);try{let e=new FormData;e.append("file",n);let t=await (0,Y.N)("".concat(p,"/tiktok_replay/upload?process=").concat(encodeURIComponent(r)),{method:"POST",body:e});f(t.message||s("上传成功","Uploaded")),await ee()}catch(e){f(e.message)}finally{P(!1)}}},ex=l||I||R,em=ew.find(e=>e.id===T),eh=!!(null==g?void 0:g.can_stream_hdmi),ep=(null==g?void 0:g.preferred_device)||(null==g?void 0:null===(t=g.video_devices)||void 0===t?void 0:t[0])||"";return(0,n.jsxs)("div",{className:"mx-auto max-w-4xl space-y-6",children:[v?(0,n.jsx)(eu,{t:s,title:s("TikTok HDMI 硬件链路","TikTok HDMI hardware chain"),subtitle:s("板端编码经 HDMI 到显示/下游设备;与「网络」页顶卡相同的箭头风格。","Encode on SoC → HDMI → display/downstream; same arrow style as Network header."),onNavigate:v,steps:[{label:"FFmpeg",sub:s("采集 / 推流","Capture"),gradient:"from-cyan-400 to-blue-500",target:"live_tiktok"},{label:"HDMI",sub:s("板载输出","SoC out"),gradient:"from-violet-500 to-fuchsia-500"},{label:"Android",sub:s("AOSP / 手机","AOSP"),gradient:"from-amber-400 to-orange-500",target:"android"}]}):null,(0,n.jsx)("div",{className:"rounded-2xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.14] via-zinc-950/80 to-fuchsia-500/10 p-6 shadow-xl shadow-cyan-900/10",children:(0,n.jsx)("div",{className:"flex flex-wrap items-start gap-4",children:(0,n.jsxs)("div",{className:"flex items-center gap-3",children:[(0,n.jsx)("div",{className:"flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/35 to-fuchsia-600/25 ring-1 ring-white/10",children:(0,n.jsx)(D.Z,{className:"h-5 w-5 text-cyan-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h2",{className:"text-lg font-semibold tracking-tight text-white",children:s("TikTok 无人直播(单路)","TikTok unmanned (single lane)")}),(0,n.jsx)("p",{className:"mt-0.5 text-xs text-zinc-500",children:s("布局对齐 YouTube 单频道:直播开关 + 源站 + URL_config;无 stream key,输出走 HDMI 硬件链路。","Same layout as YouTube single: controls + URLs; no stream key — HDMI hardware path.")})]})]})})}),(0,n.jsx)("div",{className:"rounded-2xl border border-cyan-500/20 bg-cyan-500/[0.06] p-4 ring-1 ring-white/[0.04]",children:(0,n.jsx)("p",{className:"text-xs leading-relaxed text-cyan-100/90",children:s("HDMI 互斥:后端在「开始 / 重新开始」TikTok 或 obs*.sh(SRS 拉流上屏)时,会自动停止其它正在占用 HDMI 链路的同类进程;与 YouTube 推流互不干扰。","HDMI mutex: starting TikTok or obs*.sh stops other TikTok/OBS HDMI sinks; YouTube RTMP is unaffected.")})}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/10 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-300"}),s("X86 HDMI 采集状态","X86 HDMI capture status")]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("这里直接显示主机是否识别到采集卡和 /dev/video 设备,可用于验证 ARM HDMI 进 X86 的链路。","Shows whether the host sees the capture card and /dev/video nodes for ARM-to-X86 HDMI ingest.")})]}),(0,n.jsx)("span",{className:"rounded-full px-3 py-1 text-[11px] font-semibold ring-1 ".concat(eh?"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-rose-500/25"),children:eh?s("可推流","Ready"):s("未就绪","Not ready")})]}),(0,n.jsxs)("div",{className:"mt-4 grid gap-3 md:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-4 text-[11px] text-zinc-400",children:[(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"device: "}),(0,n.jsx)("span",{className:"font-mono text-zinc-200",children:ep||"—"})]}),(0,n.jsxs)("p",{className:"mt-2",children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"video nodes: "}),(0,n.jsx)("span",{className:"font-mono text-zinc-200",children:((null==g?void 0:g.video_devices)||[]).join(", ")||"—"})]}),(0,n.jsxs)("p",{className:"mt-2",children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"usb matches: "}),(0,n.jsx)("span",{className:"text-zinc-300",children:((null==g?void 0:g.usb_capture_matches)||[]).length||0})]})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-4 text-[11px] text-zinc-400",children:[(0,n.jsx)("p",{className:"text-zinc-600",children:s("建议 FFmpeg","Suggested FFmpeg")}),(0,n.jsx)("p",{className:"mt-2 break-all font-mono text-zinc-200",children:(null==g?void 0:g.suggested_ffmpeg_cmd)||"—"}),(null==g?void 0:g.suggested_ffmpeg_cmd)?(0,n.jsxs)("button",{type:"button",onClick:()=>m(g.suggested_ffmpeg_cmd||""),className:"mt-3 inline-flex items-center gap-2 rounded-lg bg-cyan-500/15 px-3 py-2 text-xs font-medium text-cyan-100 ring-1 ring-cyan-500/25",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),s("复制命令","Copy command")]}):null]})]}),((null==g?void 0:g.issues)||[]).length?(0,n.jsx)("ul",{className:"mt-4 list-inside list-disc space-y-1 text-[11px] text-amber-200/85",children:((null==g?void 0:g.issues)||[]).map(e=>(0,n.jsx)("li",{children:e},e))}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/20 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-amber-200/90",children:[(0,n.jsx)(B.Z,{className:"h-4 w-4"}),(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("直播开关","Live control")})]}),(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:s("TikTok 进程(PM2)","TikTok PM2 process")}),(0,n.jsx)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:r,onChange:e=>a(e.target.value),children:w.map(e=>(0,n.jsxs)("option",{value:e.pm2,children:[e.label," (",e.pm2,")"]},e.pm2))}),(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:s("直播源","Video source")}),(0,n.jsxs)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:N,onChange:e=>$(e.target.value),children:[(0,n.jsx)("option",{value:"ffmpeg",hidden:!0,children:"FFmpeg(抖音 / TikTok 等直播间 URL)"}),(0,n.jsx)("option",{value:"replay",hidden:!0,children:s("录播素材(本地上传)","VOD upload")}),(0,n.jsx)("option",{value:"srs",children:s("SRS(OBS 推流)","SRS (OBS)")})]}),"srs"===N?(0,n.jsx)("p",{className:"mt-2 text-[11px] leading-relaxed text-amber-200/85",children:s("此模式不显示 URL_config:请在 OBS 中填写下方 RTMP 推流地址;板端 SRS 拉流上屏仍可能占用 HDMI,与 TikTok 进程互斥规则不变。","URL_config is hidden: set the RTMP URL in OBS. SRS pull-to-HDMI may still mutex with TikTok per server rules.")}):null,(0,n.jsx)("div",{className:"mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3",children:[["start",s("开始直播","Start"),"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30"],["stop",s("停止直播","Stop"),"bg-rose-500/15 text-rose-200 ring-rose-500/25"],["restart",s("重新开始","Restart"),"bg-amber-500/15 text-amber-200 ring-amber-500/25"]].map(e=>{let[t,s,l]=e;return(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>d(t),className:"rounded-xl px-4 py-3 text-sm font-medium ring-1 ".concat(l),children:s},t)})}),(0,n.jsxs)("p",{className:"mt-3 text-center text-[11px] text-zinc-500",children:[s("当前 HDMI 预设(备忘)","HDMI preset (memo)"),":"," ",(0,n.jsx)("span",{className:"font-mono text-cyan-300/90",children:em?s(em.labelZh,em.labelEn):T})]})]}),"replay"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/25 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(eo.Z,{className:"h-4 w-4 text-violet-300"}),s("录播素材","VOD library")]}),(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,n.jsxs)("label",{className:"inline-flex cursor-pointer items-center gap-2 rounded-xl bg-violet-500/15 px-3 py-2 text-xs font-medium text-violet-100 ring-1 ring-violet-500/30",children:[(0,n.jsx)(ec.Z,{className:"h-3.5 w-3.5"}),(0,n.jsx)("span",{children:s("上传视频","Upload")}),(0,n.jsx)("input",{type:"file",accept:".mp4,.mkv,.ts,.flv,.mov,.m4v,.webm",className:"hidden",onChange:er})]}),(0,n.jsx)("button",{type:"button",disabled:ex||!r,onClick:()=>void ee(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-3 py-2 text-xs text-zinc-300",children:s("刷新列表","Refresh")})]})]}),(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:s("文件保存在服务器 config/tiktok_replay/对应进程目录;点「开始直播」会写入 replayfile 行并启动 PM2 进程。停播请用上方「停止直播」。","Files under config/tiktok_replay/. Go live writes replayfile + starts PM2. Use Stop above to stop.")}),S?(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:s("加载中…","Loading…")}):0===k.length?(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:s("暂无文件,请先上传。","No files yet.")}):(0,n.jsx)("ul",{className:"mt-4 space-y-2",children:k.map(e=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-xs text-zinc-300",children:[(0,n.jsx)("span",{className:"min-w-0 flex-1 truncate font-mono",children:e.name}),(0,n.jsxs)("span",{className:"shrink-0 text-zinc-500",children:[(e.size/1048576).toFixed(1)," MB"]}),(0,n.jsxs)("div",{className:"flex shrink-0 flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void(async()=>{try{await Promise.resolve(u(e.replay_url,e.name))}catch(e){}})(),className:"rounded-lg bg-emerald-500/20 px-2 py-1 text-[11px] text-emerald-100 ring-1 ring-emerald-500/30",children:s("开始直播","Go live")}),(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>d("stop"),className:"rounded-lg bg-rose-500/15 px-2 py-1 text-[11px] text-rose-100 ring-1 ring-rose-500/25",children:s("停播","Stop")}),(0,n.jsxs)("button",{type:"button",disabled:ex,onClick:()=>void ei(e.name),className:"inline-flex items-center gap-1 rounded-lg bg-zinc-700/40 px-2 py-1 text-[11px] text-zinc-200",children:[(0,n.jsx)(ed.Z,{className:"h-3 w-3"}),s("删除","Del")]})]})]},e.name))})]}):null,"srs"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-emerald-500/25 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("SRS / OBS 推流","SRS / OBS ingest")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("OBS:设置 → 推流 → 服务选「自定义」,服务器填 RTMP 地址;串流密钥可与 SRS 应用配置一致(默认 live/livestream)。","OBS → Stream → Custom: RTMP URL as server; stream key per SRS app (default live/livestream).")}),(0,n.jsxs)("dl",{className:"mt-4 space-y-3 text-xs",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("RTMP 推流地址(OBS 服务器)","RTMP publish URL")}),(0,n.jsxs)("dd",{className:"mt-1 flex flex-wrap items-center gap-2 font-mono text-emerald-200/90",children:[(0,n.jsx)("span",{className:"break-all",children:b.srs_rtmp_publish||"—"}),b.srs_rtmp_publish?(0,n.jsx)("button",{type:"button",className:"shrink-0 text-cyan-300 hover:text-cyan-200",onClick:()=>m(b.srs_rtmp_publish),children:(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"})}):null]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("HTTP-FLV 预览","HTTP-FLV preview")}),(0,n.jsxs)("dd",{className:"mt-1 flex flex-wrap items-center gap-2 font-mono text-zinc-400",children:[(0,n.jsx)("span",{className:"break-all",children:b.srs_play_flv||b.srs_http||"—"}),b.srs_play_flv||b.srs_http?(0,n.jsx)("button",{type:"button",className:"shrink-0 text-cyan-300 hover:text-cyan-200",onClick:()=>m(b.srs_play_flv||b.srs_http),children:(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"})}):null]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("SRS 控制台","SRS web UI")}),(0,n.jsx)("dd",{className:"mt-1 font-mono text-zinc-400",children:(0,n.jsx)("a",{href:b.srs_http||"#",target:"_blank",rel:"noreferrer",className:"text-cyan-300 hover:underline",children:b.srs_http||"—"})})]})]})]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-400"}),s("HDMI 硬件与时序","HDMI timing")]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("点选一路作为现场备忘;与板端 ffmpeg/OBS 采集参数对齐。","Pick a preset as field memo; match capture settings.")}),(0,n.jsx)("div",{className:"mt-4 grid gap-3 md:grid-cols-3",children:ew.map(e=>{let t=L===e.id,l=T===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>{M(t?null:e.id),E(e.id)},className:"rounded-2xl border p-4 text-left transition ".concat(l?"border-cyan-500/50 ring-1 ring-cyan-500/20":""," ").concat(t?"border-cyan-500/40 bg-cyan-500/10":"border-white/[0.08] bg-zinc-950/50 hover:border-white/15"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-400"}),s(e.labelZh,e.labelEn)]}),(0,n.jsx)("p",{className:"mt-2 font-mono text-[11px] text-zinc-400",children:e.spec}),t?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:s(e.hintZh,e.hintEn)}):null]},e.id)})}),(0,n.jsxs)("div",{className:"mt-6 rounded-xl border border-white/5 bg-black/25 p-4",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-zinc-400",children:s("上线前检查清单(本地保存)","Pre-flight checklist (local)")}),(0,n.jsx)("ul",{className:"mt-3 space-y-2",children:[{id:"cable",zh:"HDMI 线材插紧、方向正确(如需转接头确认规格)",en:"HDMI cable seated; adapter specs OK"},{id:"thermal",zh:"长时间推流注意散热,过热会掉帧黑屏",en:"Thermal headroom for long runs"}].map(e=>(0,n.jsxs)("li",{className:"flex items-start gap-2 text-xs text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"mt-0.5 rounded border-white/20 bg-black/40",checked:!!V[e.id],onChange:t=>et({...V,[e.id]:t.target.checked})}),(0,n.jsx)("span",{children:s(e.zh,e.en)})]},e.id))}),(0,n.jsx)("label",{className:"mt-4 block text-[11px] text-zinc-500",children:s("HDMI / 走线备忘","HDMI / wiring notes")}),(0,n.jsx)("textarea",{className:"mt-1 min-h-[72px] w-full resize-y rounded-lg border border-white/10 bg-black/40 p-3 text-xs text-zinc-300",value:J,onChange:e=>es(e.target.value),spellCheck:!1,placeholder:s("接口、线长、显示器型号…","Ports, cable length, display model…")})]})]}),"ffmpeg"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("直播地址列表 \xb7 URL_config.ini","URL_config.ini")}),(0,n.jsxs)("button",{type:"button",className:"inline-flex items-center gap-1 text-xs text-cyan-300 hover:text-cyan-200",onClick:()=>m(o),children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),s("复制全文","Copy all")]})]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("一行一个地址;可勾选自动从服务器刷新,或点「重新读取」手动同步。","One URL per line; enable auto-refresh or reload from server.")}),(0,n.jsx)("div",{className:"mt-2 flex flex-wrap items-center gap-4 text-xs text-zinc-400",children:(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:Z,onChange:e=>O(e.target.checked)}),s("每 8 秒自动从服务器刷新","Auto-refresh from server every 8s")]})}),H?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:H}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:o,onChange:e=>{F.current=!0,X(e.target.value)},spellCheck:!1}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void en(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-300",children:s("重新读取","Reload from server")}),(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void el(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm font-medium text-cyan-100 ring-1 ring-cyan-500/30",children:s("保存 URL 配置","Save URL config")})]})]}):null,(0,n.jsx)(ev,{t:s,currentProc:r,fetchJson:h,urlListText:"ffmpeg"===N?o:"",keySuffixUi:"",showKeyRow:!1})]})}var eS=s(5468),e_=s(9397),eC=s(90);let eR="live-hub-youtube-pro-v2",eP=/[^a-zA-Z0-9_.-]+/g;function eL(e){return(e||"").trim().replace(eP,"_")}function eM(e){return eL(String(e.pm2Name||""))||eO(String(e.id||""))}function eT(e){if(!e||"object"!=typeof e)return null;let t=String(e.id||"").trim();if(!t)return null;let s=String(e.name||"").trim()||t;return{id:t,name:s,streamKeySuffix:"string"==typeof e.streamKeySuffix?e.streamKeySuffix:void 0,streamKey:"string"==typeof e.streamKey?e.streamKey:void 0,urlLines:"string"==typeof e.urlLines?e.urlLines:void 0,pm2Name:"string"==typeof e.pm2Name?e.pm2Name:void 0,notes:"string"==typeof e.notes?e.notes:void 0}}function eU(e){if(!e)return[];try{let t=JSON.parse(e);if(!Array.isArray(t))return[];return t.map(e=>eT(e)).filter(e=>null!==e)}catch(e){return[]}}function eE(e){window.localStorage.setItem(eR,JSON.stringify(e))}let eI=["/hub/youtube_pro_channels","/youtube_pro_channels"];async function eD(e){for(let s of eI)try{var t;let n=await e(s);return{fetched:!0,channels:(t=n.channels,Array.isArray(t)?t.map(e=>eT(e)).filter(e=>null!==e):[])}}catch(e){}return{fetched:!1,channels:[]}}async function eA(e,t){let s=null;for(let n of eI)try{await e(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channels:t})});return}catch(e){s=e instanceof Error?e:Error(String(e))}throw null!=s?s:Error("youtube_pro_channels sync failed")}function eZ(){return"ch_".concat(Date.now().toString(36),"_").concat(Math.random().toString(36).slice(2,7))}function eO(e){return"youtube2__".concat(eL(e)||"channel")}function eH(e){return/youtube/i.test(e.script)||/youtube/i.test(e.pm2)}function eB(e){return(e.split("/").pop()||e).replace(/\.[^.]+$/,"")}function eK(e){return eH(e)&&e.pm2.trim()===eB(e.script).trim()}function eF(e){var t;return!!e&&(/youtube/i.test(e.pm2)||/youtube/i.test(null!==(t=e.script)&&void 0!==t?t:""))}function eW(e,t){let s=((null==t?void 0:t.business_status)||"").trim().toLowerCase();return(null==t?void 0:t.is_pushing)||"streaming"===s?0:eV(null==t?void 0:t.process_status)?1:"youtube"===e.pm2.trim().toLowerCase()?2:"youtube"===eB(e.script).trim().toLowerCase()?3:10}let eY="live-hub-youtube-mode-v1",eJ=new Set(["tiktok","obs","web"]);function eG(e){var t,s,n;return{name:e.name,streamKey:null!==(t=e.streamKey)&&void 0!==t?t:"",pm2Name:null!==(s=e.pm2Name)&&void 0!==s?s:eO(e.id),notes:null!==(n=e.notes)&&void 0!==n?n:""}}function eV(e){return/^(online|running)$/i.test((e||"").trim())}function eq(e,t){let s=((null==e?void 0:e.business_status)||"").trim().toLowerCase();return"ready"===s?t("已配置","Ready"):"streaming"===s?t("推流中","Pushing"):"waiting_source"===s?t("等待上游","Waiting source"):"source_live"===s?t("源已开播,转推未起","Source live, relay down"):"monitoring"===s?t("监控中","Monitoring"):"misconfigured"===s?t("密钥异常","Key invalid"):"stale"===s?t("心跳过期","Stale"):"error"===s?t("异常","Error"):eV(null==e?void 0:e.process_status)?t("运行中","Running"):((null==e?void 0:e.process_status)||"").trim()||t("未运行","Idle")}function eX(e){return e.split(/\r?\n/).map(e=>e.trim()).filter(e=>e&&!e.startsWith("#"))}function e$(e,t){return eX(e).length>0&&0===eX(t).length}function eQ(e){let{kind:t,busyKind:s,targetPm2:l,busy:i,online:r,label:a,className:o,baseLock:c,onRun:d,notify:x,t:u,compact:m}=e,h=function(e){if(!(null==e?void 0:e.startsWith("proc:")))return null;let t=e.slice(5),s=t.indexOf(":");if(s<=0)return null;let n=t.slice(0,s),l=t.slice(s+1);return l&&["start","stop","restart"].includes(n)?{action:n,pm2:l}:null}(i),p=h&&h.pm2===l,b=null!=s?s:t,g=!!(p&&h.action===b),f=!!(p&&h.action!==b),y="start"===t&&r,w="stop"===t&&!r,N="restart"===t&&!r,j=y||w||N;return(0,n.jsxs)("button",{type:"button",onClick:()=>{if(!g){if(f){x(u("请等待当前操作完成","Wait for the current action to finish"));return}if(y){x(u("已在直播中,无需重复开始","Already live"));return}if(w){x(u("当前未在直播,无法停止","Not live — cannot stop"));return}if(N){x(u("进程未运行时请使用「开始直播」","Use Start when the process is not running"));return}c||d()}},className:"relative overflow-hidden rounded-xl ring-1 transition ".concat(m?"min-h-[2rem] px-3 py-1.5 text-xs font-medium":"min-h-[2.75rem] px-4 py-3 text-sm font-medium"," ").concat(o," ").concat(j||c||g||f?"cursor-not-allowed":""," ").concat(j&&!g?"opacity-45":""," ").concat(f?"opacity-35":""," ").concat(g?"ring-violet-400/40":""),children:[g?(0,n.jsx)("span",{className:"absolute inset-0 flex items-center justify-center bg-black/25 backdrop-blur-[2px]",children:(0,n.jsx)(v.Z,{className:"".concat(m?"h-4 w-4":"h-6 w-6"," animate-spin text-white")})}):null,(0,n.jsxs)("span",{className:"inline-flex items-center justify-center gap-1.5 ".concat(g?"invisible":""),children:[j?(0,n.jsx)(eS.Z,{className:"".concat(m?"h-3 w-3":"h-4 w-4"," shrink-0 opacity-90"),"aria-hidden":!0}):null,a]})]})}function e0(e){var t,s,l,i,r;let{t:a,busy:o,entries:d,currentProc:x,setCurrentProc:u,urlConfig:m,setUrlConfig:h,onRunProcess:p,onSaveUrlConfig:b,fetchJson:g,liveProcesses:f,notify:w,onNavigate:N,quickLinks:j}=e,k=(0,_.useMemo)(()=>d.filter(eH),[d]),z=(0,_.useMemo)(()=>k.filter(e=>eK(e)),[k]),S=(0,_.useMemo)(()=>(function(e,t){let s=e.filter(eH),n=s.filter(e=>eK(e)),l=n.length?[...n]:s.length?[...s]:[...e],i=new Set(l.map(e=>e.pm2.trim()));for(let e of t.filter(eF)){var r,a,o,c;let t=e.pm2.trim();if(!t||i.has(t))continue;let n=(e.business_status||"").trim().toLowerCase();if(!e.is_pushing&&"streaming"!==n&&!eV(e.process_status))continue;let d=s.find(e=>e.pm2.trim()===t);l.push(null!=d?d:{pm2:t,script:null!==(o=null!==(a=e.script)&&void 0!==a?a:null===(r=s[0])||void 0===r?void 0:r.script)&&void 0!==o?o:"youtube.py",label:null!==(c=e.label)&&void 0!==c?c:t}),i.add(t)}return l})(k.length?k:d,f),[d,f,k]),C=(0,_.useMemo)(()=>(function(e,t){if(!e.length)return"";let s=new Map(t.filter(eF).map(e=>[e.pm2.trim(),e])),n=e[0],l=eW(n,s.get(n.pm2.trim()));for(let t of e.slice(1)){let e=eW(t,s.get(t.pm2.trim()));e"youtube"===e.pm2.trim().toLowerCase());if(n)return n.pm2;let l=e.find(e=>"youtube"===eB(e.script).trim().toLowerCase());return l?l.pm2:null!==(s=null===(t=e[0])||void 0===t?void 0:t.pm2)&&void 0!==s?s:""}(S),[f,S]),[R,P]=(0,_.useState)("single"),[L,M]=(0,_.useState)([]),[T,U]=(0,_.useState)(""),[E,I]=(0,_.useState)(""),[D,A]=(0,_.useState)({key:"",rtmps:!0,bitrate:"",fastAudio:!1}),[O,H]=(0,_.useState)(null),[K,F]=(0,_.useState)(!1),[W,Y]=(0,_.useState)(""),[J,G]=(0,_.useState)(!0),[V,q]=(0,_.useState)(!1),[X,$]=(0,_.useState)(!1),[Q,ee]=(0,_.useState)(null),[et,es]=(0,_.useState)({}),[en,el]=(0,_.useState)(!1),[ei,er]=(0,_.useState)(null),[ea,eo]=(0,_.useState)(0),ec=(0,_.useRef)(!1),ef=(0,_.useRef)(m),ey=(0,_.useRef)(W),ew=(0,_.useRef)(x),eN=(0,_.useRef)(0),ej=(0,_.useRef)(0),ek=(0,_.useCallback)(e=>{ef.current=e,h(e)},[h]),ez=(0,_.useCallback)(e=>{ey.current=e,Y(e)},[]);(0,_.useEffect)(()=>{ec.current=!1},[x]),(0,_.useEffect)(()=>{ew.current=x},[x]),(0,_.useEffect)(()=>{ef.current=m},[m]),(0,_.useEffect)(()=>{ey.current=W},[W]);let eS=(0,_.useCallback)(async()=>{let e=ew.current.trim();if(!e)return;let t=++eN.current;try{var s,n,l;let i=new URLSearchParams({process:e,_ts:String(Date.now())}),r=await g("/get_url_config?".concat(i));if(t!==eN.current||e!==ew.current.trim())return;let a=null!==(s=r.content)&&void 0!==s?s:"";if(ec.current)return;if("single"===R)ek(a);else{let e=null!==(l=null===(n=L.find(e=>e.id===T))||void 0===n?void 0:n.urlLines)&&void 0!==l?l:"";if(e$(e,a)){ez(e);return}ez(a),T&&M(e=>{let t=e.map(e=>e.id===T?{...e,urlLines:a}:e);return eE(t),t})}}catch(e){}},[T,ez,ek,L,g,R]);(0,_.useEffect)(()=>{if(!x||!J)return;let e=window.setInterval(()=>{ec.current||eS()},8e3);return()=>clearInterval(e)},[x,J,eS]);let eP=async()=>{let e=ew.current.trim();if(!e)return;q(!0),ee(null),ec.current=!1;let t=++eN.current;try{var s,n,l;let i=new URLSearchParams({process:e,_ts:String(Date.now())}),r=await g("/get_url_config?".concat(i));if(t!==eN.current||e!==ew.current.trim())return;let o=null!==(s=r.content)&&void 0!==s?s:"";if(ec.current){ee(a("检测到本地未保存修改,已保留当前草稿","Kept the local unsaved draft"));return}if("single"===R)ek(o);else{let e=null!==(l=null===(n=L.find(e=>e.id===T))||void 0===n?void 0:n.urlLines)&&void 0!==l?l:"";if(e$(e,o)){ez(e),ee(a("服务器暂无已保存 URL,已保留本地草稿","Server has no saved URLs, kept the local draft"));return}ez(o),T&&M(e=>{let t=e.map(e=>e.id===T?{...e,urlLines:o}:e);return eE(t),t})}ee(a("已从服务器读取 URL_config","Reloaded URL_config from server"))}catch(s){if(t!==eN.current||e!==ew.current.trim())return;ee(s.message)}finally{t===eN.current&&e===ew.current.trim()&&q(!1)}};(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(eY);("pro"===e||"single"===e)&&P(e)}catch(e){}},[]),(0,_.useEffect)(()=>{let e=!1;return(async()=>{try{let t=await eD(g);if(t.fetched&&t.channels.length&&!e){M(t.channels),eE(t.channels);return}}catch(e){}if(e)return;let t=function(){let e=window.localStorage.getItem(eR);if(!e&&(e=window.localStorage.getItem("live-hub-youtube-pro-v1"))){let t=eU(e);if(t.length)return window.localStorage.setItem(eR,JSON.stringify(t)),t}return eU(e)}(),s=t.length>0?t:(()=>{let e=eZ();return[{id:e,name:"线路 1",streamKey:"",urlLines:"",pm2Name:eO(e),notes:""}]})();M(s),t.length||eE(s)})(),()=>{e=!0}},[g]),(0,_.useEffect)(()=>{if(!L.length){T&&U("");return}T&&L.some(e=>e.id===T)||U(L[0].id)},[L,T]);let eT=(0,_.useCallback)(e=>f.find(t=>t.pm2===e),[f]),eI=(0,_.useCallback)(e=>{var t;return null===(t=eT(e))||void 0===t?void 0:t.process_status},[eT]),e0=eT(x),e1=(0,_.useMemo)(()=>{var e,t;return!!x&&(/youtube/i.test(x)||/youtube/i.test(null!==(e=null==e0?void 0:e0.script)&&void 0!==e?e:"")||/youtube/i.test(null!==(t=null==e0?void 0:e0.label)&&void 0!==t?t:""))},[x,e0]);(0,_.useEffect)(()=>{"single"!==R||!C||x&&e1||u(C)},[x,e1,R,C,u]);let e5=(0,_.useCallback)(e=>{M(e),eE(e)},[]),e2=(0,_.useCallback)(async e=>{await eA(g,e)},[g]),e3=(0,_.useCallback)(e=>{e5(e),e2(e).catch(e=>{w("".concat(a("多频道列表未同步到服务器","Channel list not saved on server"),": ").concat(e.message))})},[e5,w,e2,a]),e4=L.find(e=>e.id===T)||L[0],e6=(0,_.useMemo)(()=>{var e;return e4?null!==(e=et[e4.id])&&void 0!==e?e:eG(e4):null},[e4,et]);(0,_.useEffect)(()=>{e4&&es(e=>e[e4.id]?e:{...e,[e4.id]:eG(e4)})},[e4]),(0,_.useEffect)(()=>{er(null)},[T]);let e8=(0,_.useCallback)((e,t)=>{e4&&(es(s=>{var n;return{...s,[e4.id]:{...null!==(n=s[e4.id])&&void 0!==n?n:eG(e4),[e]:t}}}),er(null))},[e4]),e9=e4?eM(e4):"",e7=e4&&e6?eM({id:e4.id,pm2Name:e6.pm2Name}):"",te=!!e4&&!!e6&&e7!==e9,tt=(0,_.useMemo)(()=>{if(!e4||!e6)return{ok:!1,message:a("请选择线路","Pick a lane"),normalizedName:"",normalizedPm2:"",normalizedStreamKey:"",normalizedNotes:""};let e=e6.name.trim()||"".concat(a("线路","Ch")," ").concat(L.findIndex(e=>e.id===e4.id)+1),t=eM({id:e4.id,pm2Name:e6.pm2Name});return t?eJ.has(t)?{ok:!1,message:a("该进程名保留给系统模块,请换一个","This PM2 name is reserved"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e6.streamKey.trim(),normalizedNotes:e6.notes.trim()}:L.find(e=>e.id!==e4.id&&eM(e)===t)?{ok:!1,message:a("该进程名已被其它线路占用","Another lane already uses this PM2 name"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e6.streamKey.trim(),normalizedNotes:e6.notes.trim()}:{ok:!0,message:"",normalizedName:e,normalizedPm2:t,normalizedStreamKey:e6.streamKey.trim(),normalizedNotes:e6.notes.trim()}:{ok:!1,message:a("进程名不能为空","PM2 name is required"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e6.streamKey.trim(),normalizedNotes:e6.notes.trim()}},[e4,e6,L,a]),ts=(0,_.useMemo)(()=>{if(!e4||!e6)return!1;let e=eG(e4);return e6.name!==e.name||e6.streamKey!==e.streamKey||eL(e6.pm2Name)!==eL(e.pm2Name)||e6.notes!==e.notes},[e4,e6]),tn=(0,_.useCallback)(e=>{var t,s,n;if("single"===e&&"pro"===R&&e4){let e=ey.current;e!==(null!==(t=e4.urlLines)&&void 0!==t?t:"").trim()&&e3(L.map(t=>t.id===e4.id?{...t,urlLines:e}:t))}if("pro"===e&&L.length){let e=L[0],t=ef.current.trim(),l=z.some(e=>e.pm2===x)&&x.trim()?x.trim():eM(e),i=[{...e,streamKey:D.key.trim()||e.streamKey||"",urlLines:t||e.urlLines||"",pm2Name:l},...L.slice(1)];e3(i);let r=null!==(s=i.find(e=>e.id===T))&&void 0!==s?s:i[0];ez((null!==(n=null==r?void 0:r.urlLines)&&void 0!==n?n:"").trim()||t)}P(e);try{window.localStorage.setItem(eY,e)}catch(e){}},[R,e4,L,ez,D.key,x,z,T,e3]),tl=(0,_.useCallback)(e=>{var t,s;let n=ey.current;e4&&"pro"===R&&n!==(null!==(t=e4.urlLines)&&void 0!==t?t:"").trim()&&e3(L.map(e=>e.id===e4.id?{...e,urlLines:n}:e)),ez((null!==(s=e.urlLines)&&void 0!==s?s:"").trim()),U(e.id),u(eM(e))},[e4,ez,R,L,u,e3]);(0,_.useEffect)(()=>{if("pro"!==R||!T)return;let e=L.find(e=>e.id===T);e&&u(eM(e))},[R,T,L,u]);let ti=(0,_.useCallback)(async()=>{let e=ew.current.trim();if(!e)return;let t=++ej.current;F(!0),H(null);try{var s;let n=new URLSearchParams({process:e,_ts:String(Date.now())}),l=await g("/get_config?".concat(n));if(t!==ej.current||e!==ew.current.trim())return;let i=null!==(s=l.content)&&void 0!==s?s:"";I(i),A(ep(i))}catch(s){if(t!==ej.current||e!==ew.current.trim())return;H(s.message)}finally{t===ej.current&&e===ew.current.trim()&&F(!1)}},[g]);(0,_.useEffect)(()=>{x&&(eS(),ti(),eo(e=>e+1))},[T,x,ti,R,eS]);let tr=(0,_.useCallback)(async()=>{var e;if(!e4||!e6)throw Error(a("请选择线路","Pick a lane"));if(!tt.ok)throw Error(tt.message);let t=ey.current,s=L.map(e=>e.id===e4.id?{...e,name:tt.normalizedName,streamKey:tt.normalizedStreamKey,pm2Name:tt.normalizedPm2,notes:tt.normalizedNotes,urlLines:t}:e);return e5(s),await e2(s),es(e=>({...e,[e4.id]:{name:tt.normalizedName,streamKey:tt.normalizedStreamKey,pm2Name:tt.normalizedPm2,notes:tt.normalizedNotes}})),{next:s,savedLane:null!==(e=s.find(e=>e.id===e4.id))&&void 0!==e?e:{...e4,name:tt.normalizedName,streamKey:tt.normalizedStreamKey,pm2Name:tt.normalizedPm2,notes:tt.normalizedNotes,urlLines:t},pm2:tt.normalizedPm2}},[e4,e6,tt,e5,L,e2,a]),ta=(0,_.useCallback)(async()=>{if(e4){el(!0),er(null);try{let e=await tr();u(e.pm2),er(a("线路设置已保存","Lane settings saved"))}catch(t){let e=t.message;er(e),w(e)}finally{el(!1)}}},[e4,w,tr,u,a]),to=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:ew.current,s=t.trim();if(!s){H(a("请选择进程","Pick a process"));return}F(!0),H(null);try{let t=new URLSearchParams({process:s});await g("/save_config?".concat(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:e})}),I(e),H(a("youtube.ini 已保存","youtube.ini saved"))}catch(e){H(e.message)}finally{F(!1)}},tc=async()=>{let e=eg(D);await to(e)},td=async()=>{F(!0),H(null);try{var e,t;let s=E||eg(D),n=await g("/config_optimize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({root:"app-config",path:(t=ew.current,"youtube.".concat(t.replace(/[^a-zA-Z0-9_.-]/g,"_"),".ini")),content:s,action:"youtube_balanced_preset"})}),l=null!==(e=n.content)&&void 0!==e?e:s;I(l),A(ep(l)),H(n.message||a("已应用 YouTube 预设","Applied YouTube preset"))}catch(e){H(e.message)}finally{F(!1)}},tx=(0,_.useMemo)(()=>{var e,t;if("single"===R)return em("key = ".concat(D.key));let s=null!==(t=null!==(e=null==e6?void 0:e6.streamKey)&&void 0!==e?e:null==e4?void 0:e4.streamKey)&&void 0!==t?t:"";return em("key = ".concat(s))},[R,D.key,null==e4?void 0:e4.streamKey,null==e6?void 0:e6.streamKey]),tu=async()=>{if(e4)try{el(!0),er(null),eN.current+=1;let e=ey.current,t=await tr();u(t.pm2),await Promise.resolve(b(e,t.pm2)),ec.current=!1,er(a("线路与地址已保存","Lane settings and URLs saved"))}catch(t){let e=t instanceof Error?t.message:String(t);er(e),w(e)}finally{el(!1)}},tm=async()=>{let e=ew.current.trim();if(!e){w(a("请选择进程","Pick a process"));return}try{eN.current+=1,await Promise.resolve(b(ef.current,e)),ec.current=!1}catch(e){}},th=async()=>{let e="single"===R?ef.current:ey.current;$(!0),ee(null);try{var t;let s=await g("/config_optimize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({root:"app-config",path:eb(ew.current),content:e,action:"dedupe_urls"})}),n=null!==(t=s.content)&&void 0!==t?t:e;"single"===R?ek(n):ez(n),ec.current=!0,ee(s.message||a("已去重当前 URL 列表","Deduplicated current URL list"))}catch(e){ee(e.message)}finally{$(!1)}},tp=async()=>{if(!e4||!e6)return;let e=ep(E||eg(D)),t=eg({key:e6.streamKey.trim(),rtmps:e.rtmps,bitrate:e.bitrate,fastAudio:e.fastAudio});el(!0),er(null);try{let e=await tr();u(e.pm2),await to(t,e.pm2),er(a("线路设置与 youtube.ini 已保存","Lane settings and youtube.ini saved"))}catch(t){let e=t.message;er(e),w(e)}finally{el(!1)}},tb="single"===R?m:W,tg=(0,_.useMemo)(()=>eX(tb).length,[tb]),tf=(0,_.useMemo)(()=>eh(tb).length,[tb]),tv=(0,_.useMemo)(()=>(0,eC.O0)(j),[j]),ty=(0,_.useMemo)(()=>(0,eC.U_)(tv,"pro"===R&&e4?L.findIndex(e=>e.id===e4.id):0),[e4,L,R,tv]),tw=ty.link?(0,ex.p)(ty.link.url):(0,ex.p)("http://live.local:9200/"),tN=(0,_.useMemo)(()=>(0,eC.Tc)(tv),[tv]),tj=(0,_.useMemo)(()=>{var e;return[null===(e=ty.link)||void 0===e?void 0:e.url,null==j?void 0:j.chromium_remote_debug,null==j?void 0:j.srs_api].filter(e=>!!e)},[j,null===(t=ty.link)||void 0===t?void 0:t.url]),tk=(0,_.useCallback)(async e=>{let t=e4?eM(e4):"";return e4&&e===t?await tr():(e5(L),await e2(L),{next:L,savedLane:null!=e4?e4:null,pm2:e})},[e4,e5,L,tr,e2]),tz=(0,_.useCallback)(async(e,t)=>{let s=!!e4&&eM(e4)===t;if("stop"!==e){var n,l;let e=L,s=t;try{let n=await tk(t);e=n.next,s=n.pm2}catch(e){w("".concat(a("当前线路保存失败","Failed to save the active lane"),": ").concat(e.message));return}let i=e.find(e=>eM(e)===s),r=null!==(n=null==i?void 0:i.urlLines)&&void 0!==n?n:"";if(!(null!==(l=null==i?void 0:i.streamKey)&&void 0!==l?l:"").trim()){w(a("请先填写当前线路的推流密钥","Fill the stream key before starting"));return}if(!eX(r).length){w(a("请先填写当前线路的直播地址","Fill the source URLs before starting"));return}t=s}s&&u(t),p(e,t)},[e4,L,tk,w,p,u,a]),tS=!!o||K||V||X||en,t_=eV(eI(x)),tC=(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap items-center gap-4 text-xs text-zinc-400",children:[(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:J,onChange:e=>G(e.target.checked)}),a("每 8 秒自动从服务器刷新","Auto-refresh from server every 8s")]}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void eP(),className:"rounded-lg border border-white/10 px-3 py-1.5 text-zinc-200 hover:bg-white/5",children:a("手动重新读取","Reload from server")}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void th(),className:"rounded-lg border border-cyan-500/20 bg-cyan-500/10 px-3 py-1.5 text-cyan-100 hover:bg-cyan-500/15",children:X?a("去重中...","Deduplicating..."):a("智能去重","Deduplicate")})]});return(0,n.jsxs)("div",{className:"mx-auto max-w-6xl space-y-6",children:[N?(0,n.jsx)(eu,{t:a,title:a("YouTube 转播链路","YouTube relay chain"),subtitle:a("与「网络」页相同风格的链路示意:代理 → 编码推流 → 浏览器侧操作工作室。","Same visual language as Network: proxy → encode → browser studio."),onNavigate:N,steps:[{label:"ShellCrash",sub:a("透明代理","Proxy"),gradient:"from-violet-500 to-fuchsia-500",target:"network"},{label:"FFmpeg",sub:a("推流 / 转码","Stream"),gradient:"from-cyan-400 to-blue-500",target:"live_youtube"},{label:"Neko",sub:ty.link?"".concat(a("Chromium 工作室","Chromium studio")," \xb7 ").concat(ty.link.label):a("Chromium 工作室","Chromium studio"),gradient:"from-emerald-400 to-teal-500",openUrl:tw}]}):null,(0,n.jsx)("div",{className:"rounded-2xl border border-violet-500/25 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/80 to-cyan-500/5 p-6 shadow-xl shadow-violet-900/10",children:(0,n.jsxs)("div",{className:"flex flex-wrap items-start justify-between gap-4",children:[(0,n.jsxs)("div",{className:"flex items-center gap-3",children:[(0,n.jsx)("div",{className:"flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-red-500/30 to-violet-600/30 ring-1 ring-white/10",children:(0,n.jsx)(c.Z,{className:"h-5 w-5 text-red-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h2",{className:"text-lg font-semibold tracking-tight text-white",children:a("YouTube 无人直播","YouTube unmanned live")}),(0,n.jsx)("p",{className:"mt-0.5 text-xs text-zinc-500",children:a("单路一条流;多路时每条线路独立保存自己的密钥、URL 列表和 PM2 任务。填好后保存,再点「开始」。Neko 浏览器仅作辅助工作室,不再阻塞推流。","Single lane or multi-lane: every lane keeps its own key, URL list, and PM2 task. Save, then Start. Neko is optional browser tooling and no longer blocks streaming.")})]})]}),(0,n.jsxs)("div",{className:"flex rounded-xl border border-white/10 bg-black/30 p-1 ".concat(""),"aria-hidden":!1,children:[(0,n.jsx)("button",{type:"button",onClick:()=>tn("single"),className:"rounded-lg px-4 py-2 text-xs font-medium transition ".concat("single"===R?"bg-violet-500/25 text-white":"text-zinc-500 hover:text-zinc-300"),children:a("单频道","Single")}),(0,n.jsx)("button",{type:"button",onClick:()=>tn("pro"),className:"rounded-lg px-4 py-2 text-xs font-medium transition ".concat("pro"===R?"bg-violet-500/25 text-white":"text-zinc-500 hover:text-zinc-300"),children:a("多频道 Pro","Multi Pro")})]})]})}),tj.length?(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[ty.link?(0,n.jsx)("a",{href:tw,target:"_blank",rel:"noreferrer",className:"rounded-lg border border-emerald-500/20 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-100 ring-1 ring-emerald-500/20",children:a("打开当前线路 Neko 工作室","Open lane Neko studio")}):null,tv.length>1?(0,n.jsx)("div",{className:"flex flex-wrap gap-2",children:tv.map(e=>{var t;return(0,n.jsxs)("a",{href:(0,ex.p)(e.url),target:"_blank",rel:"noreferrer",className:"rounded-lg border px-3 py-2 text-xs ring-1 ".concat((null===(t=ty.link)||void 0===t?void 0:t.id)===e.id?"border-emerald-400/30 bg-emerald-500/15 text-emerald-50 ring-emerald-400/25":"border-white/10 bg-white/5 text-zinc-200 ring-white/10"),children:[e.label,e.port?" :".concat(e.port):""]},e.key)})}):null,(null==j?void 0:j.chromium_remote_debug)?(0,n.jsx)("a",{href:(0,ex.p)(j.chromium_remote_debug),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-cyan-500/20 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100 ring-1 ring-cyan-500/20",children:a("打开 Chromium 调试","Open Chromium debug")}):null,(null==j?void 0:j.srs_api)?(0,n.jsx)("a",{href:(0,ex.p)(j.srs_api),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-violet-500/20 bg-violet-500/10 px-3 py-2 text-xs text-violet-100 ring-1 ring-violet-500/20",children:a("打开 SRS API","Open SRS API")}):null]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/20 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-amber-200/90",children:[(0,n.jsx)(B.Z,{className:"h-4 w-4"}),(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播开关","Live control")})]}),"single"===R?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:a("转播任务","Relay task")}),(0,n.jsxs)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:x,onChange:e=>{let t=e.target.value;ew.current=t,u(t)},children:[x&&!S.some(e=>e.pm2===x)?(0,n.jsxs)("option",{value:x,children:[x,e0?" (".concat(eq(e0,a),")"):""]}):null,S.map(e=>(0,n.jsxs)("option",{value:e.pm2,children:[function(e,t){let s=eB(e.script).toLowerCase();return s.startsWith("douyin_youtube")?t("抖音→YouTube","Douyin → YouTube"):s.startsWith("youtube")?t("YouTube","YouTube"):e.label}(e,a)," (",e.pm2,")"]},e.pm2))]}),(0,n.jsxs)("div",{className:"mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3",children:[(0,n.jsx)(eQ,{kind:"start",targetPm2:x,busy:o,online:t_,label:a("开始直播","Start"),className:"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30",baseLock:!1,onRun:()=>p("start",x),notify:w,t:a}),(0,n.jsx)(eQ,{kind:"stop",targetPm2:x,busy:o,online:t_,label:a("停止直播","Stop"),className:"bg-rose-500/15 text-rose-200 ring-rose-500/25",baseLock:!1,onRun:()=>p("stop",x),notify:w,t:a}),(0,n.jsx)(eQ,{kind:"restart",targetPm2:x,busy:o,online:t_,label:a("重新开始","Restart"),className:"bg-amber-500/15 text-amber-200 ring-amber-500/25",baseLock:!1,onRun:()=>p("restart",x),notify:w,t:a})]}),(0,n.jsxs)("p",{className:"mt-3 text-center text-xs text-zinc-400",children:[a("当前状态","Current state"),": ",eq(e0,a)]}),(0,n.jsxs)("p",{className:"mt-3 text-center font-mono text-[11px] text-zinc-600",children:["key ",a("尾码","suffix"),": ",tx||"—"]})]}):(0,n.jsx)(n.Fragment,{children:(0,n.jsxs)("div",{className:"mt-4 rounded-xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.1] via-zinc-950/50 to-violet-500/[0.06] p-4 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-cyan-200/90",children:a("Pro 转播实况","Pro relay status")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:a("对齐桌面端:每路独立 URL / youtube 配置文件;点左侧信息切换编辑,右侧控制进程。","Per-lane config files like desktop; click row info to edit, use right-side controls for PM2.")}),tN?(0,n.jsxs)("p",{className:"mt-2 text-[11px] text-zinc-500",children:[a("Neko 实例","Neko instances"),": ",tN]}):null,(0,n.jsx)("div",{className:"mt-3 max-h-[min(24rem,55vh)] space-y-2 overflow-y-auto pr-1",children:L.map(e=>{var t,s,l;let i=eM(e),r=eT(i),c=eI(i),d=eX((null!==(t=e.urlLines)&&void 0!==t?t:"").trim()),x=eh((null!==(s=e.urlLines)&&void 0!==s?s:"").trim()),u=x[0],m=em("key = ".concat(null!==(l=e.streamKey)&&void 0!==l?l:"")),h=T===e.id,p=eV(c),b=L.findIndex(t=>t.id===e.id),g=(0,eC.U_)(tv,b).link;return(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2.5 text-left text-xs transition sm:flex-nowrap ".concat(h?"border-violet-500/45 bg-violet-500/[0.1]":"border-white/10 bg-black/30 hover:border-white/18"),children:[(0,n.jsxs)("button",{type:"button",className:"min-w-0 flex-1 text-left",onClick:()=>{ew.current=eM(e),tl(e)},children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,n.jsx)("span",{className:"font-medium text-zinc-100",children:e.name}),(0,n.jsx)("span",{className:"shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ring-1 ".concat(function(e){let t=((null==e?void 0:e.business_status)||"").trim().toLowerCase();return"ready"===t?"bg-sky-500/15 text-sky-100 ring-sky-500/25":"streaming"===t?"bg-emerald-500/15 text-emerald-200 ring-emerald-500/30":"waiting_source"===t||"monitoring"===t?"bg-amber-500/15 text-amber-100 ring-amber-500/25":"source_live"===t?"bg-sky-500/15 text-sky-100 ring-sky-500/25":"misconfigured"===t||"error"===t||"stale"===t?"bg-rose-500/15 text-rose-100 ring-rose-500/25":eV(null==e?void 0:e.process_status)?"bg-emerald-500/15 text-emerald-200 ring-emerald-500/30":"bg-zinc-600/20 text-zinc-400 ring-zinc-500/25"}(r)),children:eq(r,a)}),g?(0,n.jsx)("span",{className:"shrink-0 rounded-md bg-emerald-500/15 px-2 py-0.5 text-[10px] font-medium text-emerald-100 ring-1 ring-emerald-500/25",children:g.label}):null]}),(0,n.jsxs)("div",{className:"mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 font-mono text-[10px] text-zinc-500",children:[(0,n.jsxs)("span",{children:["key …",m||"—"]}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsx)("span",{className:"truncate",title:i,children:i}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsxs)("span",{children:["URL ",d.length]}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsxs)("span",{children:["DY ",x.length]})]}),u?(0,n.jsx)("a",{href:u,target:"_blank",rel:"noreferrer",className:"mt-1 block max-w-full truncate text-cyan-300/90 underline-offset-2 hover:underline",onClick:e=>e.stopPropagation(),title:u,children:u.replace(/^https?:\/\//,"")}):(0,n.jsx)("span",{className:"mt-1 block text-zinc-600",children:"—"})]}),(0,n.jsxs)("div",{className:"flex w-full shrink-0 flex-wrap items-center justify-end gap-1.5 sm:w-auto",onClick:e=>e.stopPropagation(),onKeyDown:e=>e.stopPropagation(),children:[(0,n.jsx)(eQ,{kind:"start",targetPm2:i,busy:o,online:p,label:a("开始","Start"),className:"rounded-lg bg-emerald-500/20 text-emerald-100 ring-emerald-500/30",baseLock:!1,onRun:()=>void tz("start",i),notify:w,t:a,compact:!0}),(0,n.jsx)(eQ,{kind:"stop",targetPm2:i,busy:o,online:p,label:a("停止","Stop"),className:"rounded-lg bg-rose-500/15 text-rose-100 ring-rose-500/25",baseLock:!1,onRun:()=>void tz("stop",i),notify:w,t:a,compact:!0}),(0,n.jsx)(eQ,{kind:"restart",targetPm2:i,busy:o,online:p,label:a("重新开始","Restart"),className:"rounded-lg bg-amber-500/15 text-amber-100 ring-amber-500/25",baseLock:!1,onRun:()=>void tz("restart",i),notify:w,t:a,compact:!0}),g?(0,n.jsxs)("button",{type:"button",onClick:()=>{window.open((0,ex.p)(g.url),"_blank","noopener,noreferrer")},className:"inline-flex min-h-[2rem] items-center justify-center rounded-lg border border-emerald-500/25 bg-emerald-500/10 px-2.5 py-1.5 text-emerald-100 ring-1 ring-emerald-500/20 transition hover:bg-emerald-500/20",title:"".concat(a("打开该线路 Neko 工作室","Open this lane's Neko studio")," \xb7 ").concat(g.label).concat(g.port?" :".concat(g.port):""),"aria-label":"".concat(a("打开该线路 Neko 工作室","Open this lane's Neko studio")," \xb7 ").concat(g.label),children:[(0,n.jsx)("span",{className:"sr-only",children:a("打开该线路 Neko 工作室","Open this lane's Neko studio")}),(0,n.jsx)("span",{"aria-hidden":!0,className:"text-sm leading-none",children:"\uD83D\uDC31"}),(0,n.jsx)(Z.Z,{className:"ml-1 h-3.5 w-3.5 shrink-0","aria-hidden":!0})]}):null]})]},e.id)})})]})})]}),"single"===R?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("YouTube 相关设置","YouTube settings")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("串流参数写进 youtube.ini,无需 Google 登录。","Stream settings go to youtube.ini — no Google login.")}),(0,n.jsxs)("div",{className:"mt-4 space-y-3",children:[(0,n.jsx)("label",{className:"block text-xs text-zinc-400",children:a("串流密钥","Stream key")}),(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm font-mono text-zinc-200",spellCheck:!1,autoComplete:"off",value:D.key,onChange:e=>A(t=>({...t,key:e.target.value})),placeholder:"xxxx-xxxx-xxxx-xxxx-xxxx"}),(0,n.jsxs)("div",{className:"grid gap-3 sm:grid-cols-3",children:[(0,n.jsxs)("label",{className:"flex flex-col gap-1 text-xs text-zinc-400",children:["RTMPS",(0,n.jsxs)("select",{className:"rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-sm text-white",value:D.rtmps?"1":"0",onChange:e=>A(t=>({...t,rtmps:"1"===e.target.value})),children:[(0,n.jsx)("option",{value:"1",children:a("是(默认)","Yes")}),(0,n.jsx)("option",{value:"0",children:a("否 → RTMP","No")})]})]}),(0,n.jsxs)("label",{className:"flex flex-col gap-1 text-xs text-zinc-400",children:[a("比特率 kbps","Bitrate kbps"),(0,n.jsx)("input",{className:"rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-sm",value:D.bitrate,onChange:e=>A(t=>({...t,bitrate:e.target.value})),placeholder:"4000"})]}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2 pt-6 text-xs text-zinc-300",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:D.fastAudio,onChange:e=>A(t=>({...t,fastAudio:e.target.checked}))}),"fast_audio(",a("轻量音频","lighter audio"),")"]})]})]}),(0,n.jsxs)("div",{className:"mt-4 flex flex-wrap gap-2",children:[(0,n.jsxs)("button",{type:"button",disabled:tS,onClick:()=>void ti(),className:"inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-200",children:[(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5 ".concat(K?"animate-spin":"")}),a("重新读取","Reload")]}),(0,n.jsxs)("button",{type:"button",disabled:tS,onClick:()=>void tc(),className:"inline-flex items-center gap-2 rounded-xl bg-violet-500/25 px-4 py-2 text-sm font-medium text-violet-100 ring-1 ring-violet-500/35",children:[K?(0,n.jsx)(v.Z,{className:"h-4 w-4 animate-spin"}):null,a("保存 youtube.ini","Save youtube.ini")]}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void td(),className:"rounded-xl border border-cyan-500/20 bg-cyan-500/10 px-4 py-2 text-sm text-cyan-100",children:a("平衡预设","Balanced preset")}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>{A(ep(E)),H(a("已从下方原文同步到表单","Synced from raw"))},className:"rounded-xl border border-white/10 px-4 py-2 text-sm text-zinc-400",children:a("从原文同步表单","Parse raw")})]}),(0,n.jsxs)("details",{className:"mt-4 rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("summary",{className:"cursor-pointer text-xs text-zinc-500",children:a("高级:youtube.ini 原文","Raw youtube.ini")}),(0,n.jsx)("textarea",{className:"mt-2 h-36 w-full resize-y rounded-lg border border-white/10 bg-black/50 p-3 font-mono text-[11px] text-zinc-400",value:E,onChange:e=>I(e.target.value),spellCheck:!1}),(0,n.jsx)("button",{type:"button",disabled:tS,className:"mt-2 rounded-lg bg-cyan-500/15 px-3 py-1.5 text-xs text-cyan-200",onClick:()=>void to(E),children:a("保存原文","Save raw")})]}),O?(0,n.jsx)("p",{className:"mt-2 font-mono text-xs text-zinc-500",children:O}):null]}):(0,n.jsxs)("div",{className:"space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsxs)("h3",{className:"text-sm font-semibold text-white",children:[a("当前线路","Current lane"),e4?(0,n.jsxs)("span",{className:"ml-2 font-normal text-zinc-400",children:["\xb7 ",e4.name]}):null,ts?(0,n.jsx)("span",{className:"ml-2 rounded-md bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-100 ring-1 ring-amber-500/25",children:a("有未保存修改","Unsaved")}):null]}),e4?(0,n.jsxs)("p",{className:"mt-0.5 font-mono text-[11px] text-zinc-500",children:["PM2: ",e9,te?" → ".concat(e7):""," \xb7 ","key ",tx||"—"]}):null]}),(0,n.jsxs)("button",{type:"button",disabled:tS,className:"inline-flex items-center gap-1 rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs text-violet-100 ring-1 ring-violet-500/30",onClick:()=>{let e=eZ(),t={id:e,name:"".concat(a("线路","Ch")," ").concat(L.length+1),streamKey:"",urlLines:"",pm2Name:eO(e),notes:""};es(s=>({...s,[e]:eG(t)})),e3([...L,t]),U(e),ew.current=eM(t),u(ew.current),ez("")},children:[(0,n.jsx)(e_.Z,{className:"h-3.5 w-3.5"}),a("新增线路","Add lane")]})]}),e4?(0,n.jsxs)("div",{className:"mt-4 space-y-3 border-t border-white/5 pt-4",children:[(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm",value:null!==(s=null==e6?void 0:e6.name)&&void 0!==s?s:"",onChange:e=>e8("name",e.target.value),placeholder:a("线路名称","Lane name")}),(0,n.jsx)("label",{className:"block text-xs text-zinc-500",children:a("串流密钥","Stream key")}),(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 font-mono text-sm",spellCheck:!1,value:null!==(l=null==e6?void 0:e6.streamKey)&&void 0!==l?l:"",onChange:e=>e8("streamKey",e.target.value),placeholder:"xxxx-xxxx… / rtmp(s)://…"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void ta(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-100",children:en?a("保存中...","Saving..."):a("保存线路设置","Save lane")}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void tp(),className:"rounded-xl bg-violet-500/25 px-4 py-2 text-sm font-medium text-violet-100 ring-1 ring-violet-500/35",children:a("写入 youtube.ini","Write youtube.ini")})]}),tt.ok?null:(0,n.jsx)("p",{className:"text-xs text-rose-300",children:tt.message}),(0,n.jsxs)("details",{className:"rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("summary",{className:"cursor-pointer text-xs text-zinc-500",children:a("高级:进程名","Advanced: process name")}),(0,n.jsx)("input",{className:"mt-2 w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-xs",value:null!==(i=null==e6?void 0:e6.pm2Name)&&void 0!==i?i:eO(e4.id),onChange:e=>e8("pm2Name",e.target.value),spellCheck:!1,placeholder:"youtube2__…"}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-[11px] text-zinc-500",children:[a("实际生效","Effective"),": ",e7||"—"]}),tt.message&&!tt.ok?(0,n.jsx)("p",{className:"mt-2 text-[11px] text-rose-300",children:tt.message}):null]}),(0,n.jsx)("textarea",{className:"h-16 w-full resize-y rounded-xl border border-white/10 bg-black/40 p-3 text-xs text-zinc-300",value:null!==(r=null==e6?void 0:e6.notes)&&void 0!==r?r:"",onChange:e=>e8("notes",e.target.value),placeholder:a("备注(可选)","Notes (optional)")}),ei?(0,n.jsx)("p",{className:"text-xs text-zinc-400",children:ei}):null,(0,n.jsxs)("button",{type:"button",disabled:tS||L.length<2,className:"inline-flex items-center gap-1 text-xs text-rose-300 hover:text-rose-200 disabled:opacity-30",onClick:()=>{var e;let t=L.filter(e=>e.id!==e4.id);es(e=>{let t={...e};return delete t[e4.id],t}),e3(t),U((null===(e=t[0])||void 0===e?void 0:e.id)||"")},children:[(0,n.jsx)(ed.Z,{className:"h-3.5 w-3.5"}),a("删除此线路","Delete lane")]})]}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播地址列表","URL list")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("每条线路独立写入自己的 URL_config..ini,不再复用或回填所谓“全局地址”。","Each lane writes to its own URL_config..ini with no shared global URL draft.")}),(0,n.jsx)("p",{className:"mt-1 font-mono text-[10px] text-zinc-600",children:x?eb(x):"URL_config.ini"}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2 text-[11px]",children:[(0,n.jsxs)("span",{className:"rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100",children:[a("有效地址","Valid URLs"),": ",tg]}),(0,n.jsxs)("span",{className:"rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100",children:["Douyin: ",tf]})]}),tC,Q?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:Q}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[160px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:W,onChange:e=>{ec.current=!0,ez(e.target.value)},spellCheck:!1,placeholder:"https://live.douyin.com/..."}),(0,n.jsx)("div",{className:"mt-3 flex flex-wrap gap-2",children:(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void tu(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:a("保存地址","Save URLs")})})]})]}),"single"===R?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播地址列表 \xb7 URL_config.ini","URL_config.ini")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("一行一个地址;可自动刷新或手动重新读取。","One URL per line; auto-refresh or reload.")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2 text-[11px]",children:[(0,n.jsxs)("span",{className:"rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100",children:[a("有效地址","Valid URLs"),": ",tg]}),(0,n.jsxs)("span",{className:"rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100",children:["Douyin: ",tf]})]}),tC,Q?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:Q}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:m,onChange:e=>{ec.current=!0,ek(e.target.value)},spellCheck:!1}),(0,n.jsx)("button",{type:"button",disabled:tS,onClick:()=>void tm(),className:"mt-3 rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:a("保存 URL 配置","Save URL config")})]}):null,(0,n.jsx)(ev,{t:a,currentProc:x,fetchJson:g,urlListText:"single"===R?m:W,keySuffixUi:tx,showKeyRow:!0,pollMs:800,refreshToken:ea})]})}var e1=s(257);let e5=()=>void 0!==e1&&e1.env.NEXT_PUBLIC_API_URL||"";function e2(e){if(null==e||!Number.isFinite(e))return"—";if(0===e)return"0 B";let t=["B","KB","MB","GB","TB"],s=e,n=0;for(;s>=1024&&n=10||0===n?0:1)," ").concat(t[n])}function e3(e){let t=e2(e);return"—"===t?t:"".concat(t,"/s")}function e4(e){return"online"===e||"running"===e?"bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30":"configured"===e?"bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/25":"skipped"===e?"bg-slate-500/15 text-slate-300 ring-1 ring-slate-500/30":"stopped"===e||"not_found"===e?"bg-zinc-500/15 text-zinc-400 ring-1 ring-zinc-500/25":"error"===e?"bg-rose-500/15 text-rose-300 ring-1 ring-rose-500/35":"bg-violet-500/15 text-violet-200 ring-1 ring-violet-500/30"}function e6(e,t){let s=(e||"").trim().toLowerCase();return"online"===s||"running"===s?t("运行中","Running"):"configured"===s?t("已配置","Configured"):"not_found"===s?t("未注册","Unregistered"):"stopped"===s?t("已停止","Stopped"):"error"===s?t("异常","Error"):e||"—"}function e8(){var e,t,s,L,M,T,U,E,I,D,A,Z,O,H,B,K,F,W,J,G,V,q,X,$,Q,ee,et,es,en,el,ei,ea,eo,ec,ed,eu,em,eh,ep,eb,eg,ef,ev,ey,ew,eN,ej,ek,eS,e_,eR,eP,eL,eM,eT,eU,eE,eI,eD;let eA=e5(),[eZ,eO]=(0,_.useState)("zh"),[eH,eB]=(0,_.useState)("dark"),[eK,eF]=(0,_.useState)("dashboard"),[eW,eY]=(0,_.useState)(!1),[eJ,eG]=(0,_.useState)(null),[eV,eq]=(0,_.useState)(null),[eX,e$]=(0,_.useState)(!0),[eQ,e1]=(0,_.useState)(null),[e8,e9]=(0,_.useState)(null),[e7,te]=(0,_.useState)(null),[tt,ts]=(0,_.useState)(!0),[tn,tl]=(0,_.useState)([]),[ti,tr]=(0,_.useState)([]),[ta,to]=(0,_.useState)(""),[tc,td]=(0,_.useState)(""),[tx,tu]=(0,_.useState)("/sdcard/app.apk"),[tm,th]=(0,_.useState)(""),[tp,tb]=(0,_.useState)(null),[tg,tf]=(0,_.useState)(null),[tv,ty]=(0,_.useState)(!0),[tw,tN]=(0,_.useState)(!0),tj=(0,_.useRef)(null),tk=(0,_.useRef)(null),tz=(0,_.useRef)(ta),tS=(0,_.useRef)(0),t_=(0,_.useRef)(null),tC=(0,_.useCallback)((e,t)=>"zh"===eZ?e:t,[eZ]),tR=(0,_.useCallback)(e=>{e9(e),null!=t_.current&&window.clearTimeout(t_.current),t_.current=window.setTimeout(()=>{e9(null),t_.current=null},2800)},[]),tP=(0,_.useCallback)(async(e,t)=>(0,Y.N)("".concat(eA).concat(e),t),[eA]),tL=(0,_.useCallback)(async()=>{if(tj.current)return tj.current;let e=(async()=>{try{var e;let[t,s]=await Promise.allSettled([tP("/hub/dashboard"),tP("/deploy/check")]);if("fulfilled"!==t.status)throw t.reason;let n=t.value;eG(n),eq(null),te(Date.now()),"fulfilled"===s.status?tf(s.value):tf(null);let l=(null===(e=n.android)||void 0===e?void 0:e.devices)||[];if(l.length&&!tm){let e=l.find(e=>"device"===(e.state||"").toLowerCase());th((null!=e?e:l[0]).serial)}}catch(e){eq(e.message),tf(null)}finally{e$(!1),tj.current=null}})();return tj.current=e,e},[tm,tP]),tM=(0,_.useMemo)(()=>{let e=new URLSearchParams({light:"1"});return"live_youtube"===eK?e.set("family","youtube"):"live_tiktok"===eK&&e.set("family","tiktok"),"/process_monitor?".concat(e.toString())},[eK]),tT=(0,_.useCallback)(async()=>{if(tk.current)return tk.current;let e=(async()=>{try{let e=(await tP(tM)).entries||[];tr(e),tl(e.map(e=>({pm2:e.pm2,script:e.script,label:e.label}))),te(Date.now()),to(t=>!e.length||t&&e.some(e=>e.pm2===t)?t:e[0].pm2)}catch(e){}finally{tk.current=null}})();return tk.current=e,e},[tP,tM]);(0,_.useEffect)(()=>{let e=window.localStorage.getItem("live-hub-lang");("zh"===e||"en"===e)&&eO(e);let t=window.localStorage.getItem("live-hub-theme");("light"===t||"dark"===t)&&eB(t)},[]),(0,_.useEffect)(()=>{window.localStorage.setItem("live-hub-lang",eZ)},[eZ]),(0,_.useEffect)(()=>{document.documentElement.setAttribute("data-theme",eH);try{window.localStorage.setItem("live-hub-theme",eH)}catch(e){}},[eH]),(0,_.useEffect)(()=>{let e=()=>ts("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{if(!eW)return;let e=e=>{"Escape"===e.key&&eY(!1)};window.addEventListener("keydown",e);let t=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{window.removeEventListener("keydown",e),document.body.style.overflow=t}},[eW]),(0,_.useEffect)(()=>()=>{null!=t_.current&&window.clearTimeout(t_.current)},[]),(0,_.useEffect)(()=>{try{"0"===localStorage.getItem("livehub.autorefresh")&&ty(!1),"0"===localStorage.getItem("livehub.showStreamProbes")&&tN(!1)}catch(e){}},[]),(0,_.useEffect)(()=>{tL()},[tL]),(0,_.useEffect)(()=>{tT()},[tT]),(0,_.useEffect)(()=>{if(!tv||!tt||"live_youtube"===eK||"live_tiktok"===eK)return;let e=window.setInterval(()=>{tL()},1e4);return()=>clearInterval(e)},[tv,tt,tL,eK]),(0,_.useEffect)(()=>{if(!tv||!tt)return;let e=window.setInterval(()=>{tT()},"live_youtube"===eK||"live_tiktok"===eK?700:4e3);return()=>clearInterval(e)},[tv,tt,tT,eK]),(0,_.useEffect)(()=>{tz.current=ta},[ta]),(0,_.useEffect)(()=>{ta&&"live_youtube"===eK&&(async()=>{let e=tz.current.trim(),t=++tS.current;try{let s=new URLSearchParams({process:e,_ts:String(Date.now())}),n=await tP("/get_url_config?".concat(s));if(t!==tS.current||e!==tz.current.trim())return;td(n.content||"")}catch(e){}})()},[ta,tP,eK]),(0,_.useEffect)(()=>{"settings"===eK&&(async()=>{try{let e=await tP("/hub/config");tb(e)}catch(e){tb(null)}})()},[tP,eK]);let tU=(0,_.useMemo)(()=>{var e;let t=(null==eJ?void 0:null===(e=eJ.stack)||void 0===e?void 0:e.quick_links)||{},s={};for(let[e,n]of Object.entries(t))/^https?:\/\//i.test(n)?s[e]=(0,ex.p)(n):s[e]=n;return s},[null==eJ?void 0:null===(e=eJ.stack)||void 0===e?void 0:e.quick_links]),tE=(0,_.useMemo)(()=>(0,eC.O0)(tU),[tU]),tI=(0,_.useMemo)(()=>(0,eC.Tc)(tE),[tE]),tD=async(e,t)=>{e1("svc:".concat(e));try{let s=await tP("/service_action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:e,action:t})});"error"===s.status?tR(s.detail||s.message||tC("操作失败","Action failed")):tR(s.message||"OK"),await tL()}catch(e){tR(e.message)}finally{e1(null)}},tA=async(e,t)=>{let s=(null!=t?t:ta).trim();if(s){e1("proc:".concat(e,":").concat(s));try{let t=await tP("/".concat(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({process:s})});tR(String(t.output||t.message||"OK")),await tT(),"live_youtube"===eK||"live_tiktok"===eK?tL():await tL()}catch(e){tR(e.message)}finally{e1(null)}}},tZ=async(e,t)=>{let s=(null!=t?t:ta).trim();if(!s)return;let n=null!=e?e:tc,l=++tS.current;e1("save:url");try{let e=new URLSearchParams({process:s});await tP("/save_url_config?".concat(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:n})}),l===tS.current&&s===tz.current.trim()&&td(n),tR(tC("URL 配置已保存","URL config saved"))}catch(e){throw tR(e.message),e}finally{e1(null)}},tO=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!tm){tR(tC("请选择设备","Pick a device"));return}e1("adb:".concat(e));try{let s=await tP("/android/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,serial:tm,payload:t})});tR(s.message||"OK")}catch(e){tR(e.message)}finally{e1(null)}},tH=[{id:"dashboard",icon:r.Z,labelZh:"总览",labelEn:"Dashboard"},{id:"services",icon:a.Z,labelZh:"服务",labelEn:"Services"},{id:"live_youtube",icon:o.Z,labelZh:"YouTube 无人直播",labelEn:"YouTube unmanned"},{id:"live_tiktok",icon:c.Z,labelZh:"TikTok 无人直播",labelEn:"TikTok unmanned"},{id:"android",icon:d.Z,labelZh:"安卓",labelEn:"Android"},{id:"network",icon:x.Z,labelZh:"网络",labelEn:"Network"},{id:"settings",icon:u.Z,labelZh:"设置",labelEn:"Settings"}],tB=e=>{(async()=>{var t;if(null===(t=navigator.clipboard)||void 0===t?void 0:t.writeText){await navigator.clipboard.writeText(e);return}let s=document.createElement("textarea");s.value=e,s.readOnly=!0,s.style.position="fixed",s.style.opacity="0",document.body.appendChild(s),s.select(),document.execCommand("copy"),s.remove()})().then(()=>tR(tC("已复制","Copied")),()=>tR(tC("复制失败","Copy failed")))},tK=null!==(ev=null==eJ?void 0:null===(t=eJ.services)||void 0===t?void 0:t.filter(e=>e.running&&e.available).length)&&void 0!==ev?ev:0,tF=null!==(ey=null==eJ?void 0:null===(s=eJ.services)||void 0===s?void 0:s.length)&&void 0!==ey?ey:0,tW=e7?new Date(e7).toLocaleTimeString("zh"===eZ?"zh-CN":"en-US",{hour12:!1}):tC("未同步","Not synced"),tY=tt?tC("前台自动刷新","Auto refresh"):tC("后台已暂停刷新","Refresh paused in background");return(0,n.jsxs)("div",{className:"lp-root min-h-screen transition-colors duration-300 ".concat("light"===eH?"bg-slate-100 text-slate-900 selection:bg-violet-400/25":"bg-[#050508] text-zinc-100 selection:bg-violet-500/30"),children:[(0,n.jsx)("div",{className:"lp-noise pointer-events-none fixed inset-0 z-0 ".concat("light"===eH?"opacity-[0.06]":"opacity-[0.22]"),"aria-hidden":!0}),(0,n.jsx)("div",{className:"lp-aurora pointer-events-none fixed -left-1/4 top-0 h-[42rem] w-[42rem] rounded-full blur-[120px] ".concat("light"===eH?"bg-violet-400/15":"bg-violet-600/20"),"aria-hidden":!0}),(0,n.jsx)("div",{className:"lp-aurora2 pointer-events-none fixed bottom-0 right-0 h-[36rem] w-[36rem] rounded-full blur-[100px] ".concat("light"===eH?"bg-cyan-400/10":"bg-cyan-500/10"),"aria-hidden":!0}),e8?(0,n.jsx)(l.E.div,{initial:{opacity:0,y:16},animate:{opacity:1,y:0},role:"status","aria-live":"polite","aria-atomic":"true",className:"fixed bottom-6 left-1/2 z-[100] max-w-md -translate-x-1/2 rounded-2xl border border-white/10 bg-zinc-950/95 px-5 py-3 text-center text-sm text-zinc-100 shadow-2xl shadow-black/50 backdrop-blur-xl",children:e8}):null,(0,n.jsxs)("div",{className:"relative z-10 flex min-h-screen",children:[(0,n.jsxs)("aside",{className:"hidden w-64 shrink-0 flex-col border-r px-4 py-6 backdrop-blur-2xl lg:flex ".concat("light"===eH?"border-slate-200/90 bg-white/70":"border-white/[0.06] bg-zinc-950/40"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 px-2",children:[(0,n.jsx)("div",{className:"flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg shadow-violet-500/25",children:(0,n.jsx)(m.Z,{className:"h-4 w-4 text-white"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-[0.2em] ".concat("light"===eH?"text-slate-500":"text-zinc-500"),children:"live.local"}),(0,n.jsx)("p",{className:"text-sm font-semibold ".concat("light"===eH?"text-slate-900":"text-white"),children:tC("无人直播系统","Unmanned Live System")})]})]}),(0,n.jsx)("nav",{className:"mt-10 flex flex-1 flex-col gap-1",children:tH.map(e=>{let t=e.icon,s=eK===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>eF(e.id),"aria-current":s?"page":void 0,className:"flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-medium transition ".concat("light"===eH?s?"bg-violet-100/90 text-slate-900 shadow-inner shadow-violet-200/50":"text-slate-600 hover:bg-slate-200/60 hover:text-slate-900":s?"bg-white/[0.08] text-white shadow-inner shadow-white/5":"text-zinc-500 hover:bg-white/[0.04] hover:text-zinc-200"),children:[(0,n.jsx)(t,{className:"h-4 w-4 ".concat("light"===eH?s?"text-violet-600":"text-slate-400":s?"text-violet-300":"text-zinc-600")}),tC(e.labelZh,e.labelEn),s?(0,n.jsx)(h.Z,{className:"ml-auto h-4 w-4 ".concat("light"===eH?"text-slate-400":"text-zinc-600")}):null]},e.id)})})]}),(0,n.jsxs)("div",{className:"flex min-w-0 flex-1 flex-col",children:[(0,n.jsxs)("header",{className:"sticky top-0 z-20 flex items-center justify-between gap-3 border-b px-4 py-3 backdrop-blur-xl lg:px-8 ".concat("light"===eH?"border-slate-200/80 bg-white/75 text-slate-900":"border-white/[0.06] bg-zinc-950/70 text-white"),children:[(0,n.jsxs)("div",{className:"flex min-w-0 items-center gap-3",children:[(0,n.jsx)("button",{type:"button",className:"rounded-xl border p-2 lg:hidden ".concat("light"===eH?"border-slate-200 bg-white/80":"border-white/10 bg-white/5"),onClick:()=>eY(!0),"aria-label":"Menu",children:(0,n.jsx)(p.Z,{className:"h-5 w-5"})}),(0,n.jsxs)("div",{className:"min-w-0",children:[(0,n.jsx)("h1",{className:"truncate text-lg font-semibold tracking-tight lg:text-xl ".concat("light"===eH?"text-slate-900":"text-white"),children:tC((null===(L=tH.find(e=>e.id===eK))||void 0===L?void 0:L.labelZh)||"",(null===(M=tH.find(e=>e.id===eK))||void 0===M?void 0:M.labelEn)||"")}),(0,n.jsxs)("p",{className:"truncate text-xs ".concat("light"===eH?"text-slate-500":"text-zinc-500"),children:[(null==eJ?void 0:null===(T=eJ.stack)||void 0===T?void 0:T.mdns_host)||"live.local"," \xb7"," ","dark"===eH?tC("夜间","Dark mode"):tC("日间","Light mode")," \xb7 ",tY," \xb7"," ",tC("最后同步","Last sync")," ",tW]})]})]}),(0,n.jsxs)("div",{className:"flex shrink-0 items-center gap-1.5 sm:gap-2",children:[(0,n.jsx)("button",{type:"button",onClick:()=>eB(e=>"dark"===e?"light":"dark"),className:"rounded-xl border p-2 transition ".concat("light"===eH?"border-slate-200 bg-white text-amber-500 shadow-sm":"border-white/10 bg-white/[0.06] text-violet-200"),title:"dark"===eH?tC("切换到日间","Switch to light"):tC("切换到夜间","Switch to dark"),"aria-label":"dark"===eH?tC("日间模式","Light mode"):tC("夜间模式","Dark mode"),"aria-pressed":"light"===eH,children:"dark"===eH?(0,n.jsx)(b.Z,{className:"h-5 w-5",strokeWidth:2}):(0,n.jsx)(g.Z,{className:"h-5 w-5",strokeWidth:2})}),(0,n.jsxs)("button",{type:"button",onClick:()=>eO(e=>"zh"===e?"en":"zh"),className:"relative rounded-xl border p-2 transition ".concat("light"===eH?"border-slate-200 bg-white text-cyan-600 shadow-sm":"border-white/10 bg-white/[0.06] text-cyan-200"),title:"zh"===eZ?"English":"中文","aria-label":tC("切换语言","Toggle language"),"aria-pressed":"en"===eZ,children:[(0,n.jsx)(f.Z,{className:"h-5 w-5",strokeWidth:2}),(0,n.jsx)("span",{className:"absolute -bottom-0.5 -right-0.5 flex h-4 min-w-[1rem] items-center justify-center rounded px-0.5 text-[9px] font-bold leading-none ".concat("light"===eH?"bg-cyan-100 text-cyan-800":"bg-cyan-500/30 text-cyan-100"),children:"zh"===eZ?"中":"A"})]}),(0,n.jsxs)("button",{type:"button",disabled:!!eQ,onClick:()=>void Promise.all([tL(),tT()]),className:"flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-medium transition disabled:opacity-40 ".concat("light"===eH?"border-slate-200 bg-white text-slate-700 hover:bg-slate-50":"border-white/10 bg-white/[0.04] text-zinc-300 hover:bg-white/[0.07]"),children:[eQ?(0,n.jsx)(v.Z,{className:"h-3.5 w-3.5 animate-spin"}):(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5"}),(0,n.jsx)("span",{className:"hidden sm:inline",children:tC("刷新","Refresh")})]})]})]}),(0,n.jsx)("main",{className:"flex-1 px-4 py-6 lg:px-8 lg:py-8",children:(0,n.jsx)(i.M,{mode:"wait",children:(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},exit:{opacity:0,y:-6},transition:{duration:.28,ease:[.22,1,.36,1]},children:["dashboard"===eK?(0,n.jsxs)("div",{className:"space-y-6",children:[eV?(0,n.jsxs)("div",{className:"rounded-2xl border border-rose-500/25 bg-rose-950/20 p-4 text-sm text-rose-200",children:[tC("无法加载仪表盘:","Dashboard error: "),eV]}):null,(0,n.jsx)(l.E.div,{initial:{opacity:0,y:8},animate:{opacity:1,y:0},transition:{delay:.02},className:"rounded-2xl border p-2 backdrop-blur-xl ".concat("light"===eH?"border-slate-200/90 bg-white/80 shadow-sm shadow-slate-200/30":"border-white/[0.07] bg-zinc-950/30"),children:(0,n.jsx)(R.Z,{snapshot:null!==(ew=null==eJ?void 0:eJ.snapshot)&&void 0!==ew?ew:null,theme:eH,tr:tC})}),(0,n.jsx)(l.E.div,{initial:{opacity:0,y:8},animate:{opacity:1,y:0},transition:{delay:.05},className:"rounded-2xl border p-3 shadow-lg ".concat("light"===eH?"border-fuchsia-200/80 bg-gradient-to-br from-fuchsia-50/90 via-white to-cyan-50/70 shadow-fuchsia-100/40":"border-fuchsia-500/20 bg-gradient-to-br from-fuchsia-500/10 via-zinc-950/60 to-cyan-500/10 shadow-fuchsia-900/10"),children:(0,n.jsx)(C,{tr:tC})}),(0,n.jsx)("div",{className:"grid gap-2 sm:grid-cols-2 xl:grid-cols-5",children:[{icon:w.Z,label:tC("CPU 1m","CPU 1m"),value:null!==(eN=null==eJ?void 0:null===(I=eJ.snapshot)||void 0===I?void 0:null===(E=I.cpu_load)||void 0===E?void 0:null===(U=E["1m"])||void 0===U?void 0:U.toFixed(2))&&void 0!==eN?eN:"—",sub:tC("负载均值","Load avg")},{icon:r.Z,label:tC("内存","Memory"),value:e2(null==eJ?void 0:null===(A=eJ.snapshot)||void 0===A?void 0:null===(D=A.memory)||void 0===D?void 0:D.available),sub:"/ ".concat(e2(null==eJ?void 0:null===(O=eJ.snapshot)||void 0===O?void 0:null===(Z=O.memory)||void 0===Z?void 0:Z.total))},{icon:N.Z,label:tC("磁盘可用","Disk free"),value:e2(null==eJ?void 0:null===(B=eJ.snapshot)||void 0===B?void 0:null===(H=B.disk)||void 0===H?void 0:H.free),sub:"/ ".concat(e2(null==eJ?void 0:null===(F=eJ.snapshot)||void 0===F?void 0:null===(K=F.disk)||void 0===K?void 0:K.total))},{icon:a.Z,label:tC("服务在线","Services up"),value:"".concat(tK,"/").concat(tF||"—"),sub:tC("基础服务层","Infra layer")},{icon:j.Z,label:tC("运行时间","Uptime"),value:function(e){if(null==e||!Number.isFinite(e))return"—";let t=Math.max(0,Math.floor(e)),s=Math.floor(t/86400),n=Math.floor(t%86400/3600),l=Math.floor(t%3600/60);return s>0?"".concat(s,"d ").concat(n,"h"):n>0?"".concat(n,"h ").concat(l,"m"):"".concat(l,"m")}(null==eJ?void 0:null===(W=eJ.snapshot)||void 0===W?void 0:W.uptime_seconds),sub:tC("自开机","Since boot")}].map((e,t)=>(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:12},animate:{opacity:1,y:0},transition:{delay:.05*t,duration:.35,ease:[.22,1,.36,1]},whileHover:{y:-2},className:"group relative overflow-hidden rounded-xl border border-white/[0.07] bg-gradient-to-br from-white/[0.05] to-transparent p-3 shadow-lg shadow-black/20",children:[(0,n.jsx)("div",{className:"absolute -right-4 -top-4 h-16 w-16 rounded-full bg-violet-500/10 blur-xl transition group-hover:bg-violet-500/18"}),(0,n.jsx)(e.icon,{className:"h-4 w-4 text-violet-300/90"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:e.label}),(0,n.jsx)("p",{className:"mt-0.5 font-mono text-lg font-semibold leading-tight text-white",children:eX?"…":e.value}),(0,n.jsx)("p",{className:"mt-0.5 text-[10px] text-zinc-500",children:e.sub})]},e.label))}),(null==eJ?void 0:eJ.hardware)&&"object"==typeof eJ.hardware?(0,n.jsxs)("div",{className:"rounded-2xl border border-indigo-500/25 bg-gradient-to-br from-indigo-500/[0.1] via-zinc-950/70 to-violet-500/[0.08] p-6 shadow-2xl shadow-indigo-500/5",children:[(0,n.jsx)("h3",{className:"text-base font-semibold text-white",children:tC("系统硬件与软件能力","Hardware & software profile")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:tC("来自 hardware_probe;用于直播/解码策略参考。","From hardware_probe; guides decode/streaming posture.")}),(0,n.jsxs)("div",{className:"mt-5 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-indigo-300/90",children:tC("平台","Platform")}),(0,n.jsxs)("dl",{className:"space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"arch"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(ej=eJ.hardware.arch)&&void 0!==ej?ej:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"kernel"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(eS=null!==(ek=eJ.hardware.kernel)&&void 0!==ek?ek:null===(J=eJ.stack)||void 0===J?void 0:J.kernel)&&void 0!==eS?eS:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"Python"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(e_=null===(G=eJ.stack)||void 0===G?void 0:G.python)&&void 0!==e_?e_:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"mDNS"}),(0,n.jsx)("dd",{className:"text-right text-cyan-200/90",children:null!==(eR=null===(V=eJ.stack)||void 0===V?void 0:V.mdns_host)&&void 0!==eR?eR:"—"})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"IP"}),(0,n.jsx)("dd",{className:"text-right text-zinc-300",children:((null===(q=eJ.stack)||void 0===q?void 0:q.ips)||[]).join(" \xb7 ")||"—"})]})]})]}),(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-fuchsia-300/90",children:"GPU / NPU"}),(()=>{let e=eJ.hardware.gpu,t=eJ.hardware.npu;return(0,n.jsxs)("dl",{className:"space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"GPU"}),(0,n.jsxs)("dd",{className:"mt-0.5 text-zinc-200",children:[(null==e?void 0:e.present)?tC("已检测","present"):tC("未检测","absent"),(null==e?void 0:e.driver)?" \xb7 ".concat(e.driver):""]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"render"}),(0,n.jsx)("dd",{className:"mt-0.5 break-all text-zinc-500",children:((null==e?void 0:e.render_nodes)||[]).join(", ")||"—"})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"NPU"}),(0,n.jsx)("dd",{className:"mt-0.5 break-all text-zinc-500",children:(null==t?void 0:t.present)?(t.devices||[]).join(", "):tC("—","—")})]})]})})()]}),(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4 lg:col-span-2",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-emerald-300/90",children:"ffmpeg / 建议策略"}),(()=>{var e,t,s,l;let i=eJ.hardware.ffmpeg_hwaccels||[],r=eJ.hardware.recommendation;return(0,n.jsxs)("div",{className:"space-y-2 text-[11px] text-zinc-400",children:[(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"hwaccels: "}),(0,n.jsx)("span",{className:"font-mono text-emerald-200/90",children:i.length?i.join(", "):"—"})]}),(0,n.jsxs)("div",{className:"grid gap-2 sm:grid-cols-2",children:[(0,n.jsxs)("p",{children:[(0,n.jsxs)("span",{className:"text-zinc-600",children:[tC("解码","Decoder"),": "]}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(e=null==r?void 0:r.video_decoder)&&void 0!==e?e:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"HDMI: "}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(t=null==r?void 0:r.hdmi_player)&&void 0!==t?t:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"Browser: "}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(s=null==r?void 0:r.browser_hwaccel)&&void 0!==s?s:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsxs)("span",{className:"text-zinc-600",children:[tC("稳定模式","Stability"),": "]}),(0,n.jsx)("span",{className:"text-amber-200/90",children:null!==(l=null==r?void 0:r.stability_mode)&&void 0!==l?l:"—"})]})]}),((null==r?void 0:r.degradation_reasons)||[]).length?(0,n.jsx)("ul",{className:"mt-2 list-inside list-disc space-y-1 text-[10px] text-amber-200/80",children:((null==r?void 0:r.degradation_reasons)||[]).map(e=>(0,n.jsx)("li",{children:e},e))}):null]})})()]})]})]}):null,tg?(0,n.jsxs)("div",{className:"rounded-2xl border p-5 ".concat(tg.ready?"border-emerald-500/25 bg-gradient-to-br from-emerald-500/[0.08] via-zinc-950/50 to-cyan-500/[0.05]":"border-amber-500/30 bg-gradient-to-br from-amber-500/[0.1] via-zinc-950/55 to-rose-500/[0.05]"),children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:tC("安装验收","Deployment health")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:tC("一键脚本完成后,本机 API、局域网入口、反向代理与 mDNS 的可用性检查。","Post-install checks for loopback API, LAN access, reverse proxy, and mDNS.")})]}),(0,n.jsx)("span",{className:"rounded-full px-3 py-1 text-[10px] font-semibold ".concat(tg.ready?"bg-emerald-500/20 text-emerald-100 ring-1 ring-emerald-500/30":"bg-amber-500/20 text-amber-100 ring-1 ring-amber-500/30"),children:tg.ready?tC("可交付","Ready"):tC("需处理","Needs attention")})]}),(0,n.jsxs)("div",{className:"mt-4 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:tC("验收摘要","Acceptance summary")}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-sm text-white",children:[null!==(eP=tg.pass_count)&&void 0!==eP?eP:0,"/",null!==(eL=tg.total_count)&&void 0!==eL?eL:0,(0,n.jsx)("span",{className:"ml-2 text-[11px] text-zinc-500",children:tC("已通过","checks passed")})]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[tC("必需项","Required"),": ",null!==(eM=tg.required_total)&&void 0!==eM?eM:0," \xb7 mDNS"," ",tg.mdns_enabled?tC("已启用","enabled"):tC("已关闭","disabled")," \xb7 edge"," ",tg.live_edge_enabled?tC("已启用","enabled"):tC("已关闭","disabled")]}),(0,n.jsx)("div",{className:"mt-3 flex flex-wrap gap-2",children:Object.entries(tg.urls||{}).filter(e=>{let[,t]=e;return!!t}).slice(0,4).map(e=>{let[t,s]=e;return(0,n.jsx)("a",{href:(0,ex.p)(s),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-[11px] text-zinc-200 ring-1 ring-white/10",children:t},t)})})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:(null===(X=tg.failing_checks)||void 0===X?void 0:X.length)?tC("失败项","Failing checks"):tC("当前状态","Current state")}),(null===($=tg.failing_checks)||void 0===$?void 0:$.length)?(0,n.jsx)("ul",{className:"mt-2 space-y-2 text-[11px] text-zinc-300",children:tg.failing_checks.slice(0,4).map(e=>(0,n.jsxs)("li",{className:"rounded-lg border border-amber-500/20 bg-amber-500/10 px-3 py-2",children:[(0,n.jsx)("span",{className:"font-semibold text-amber-100",children:e.label}),(0,n.jsx)("span",{className:"ml-2 text-zinc-400",children:e.detail||"—"})]},e.id))}):(0,n.jsx)("p",{className:"mt-2 text-[11px] text-emerald-200/90",children:tC("必需检查已全部通过,可直接用浏览器面板交付。","All required checks pass; browser control plane is ready.")}),(tg.hints||[]).length?(0,n.jsx)("ul",{className:"mt-3 list-inside list-disc space-y-1 text-[10px] text-zinc-400",children:(tg.hints||[]).slice(0,3).map(e=>(0,n.jsx)("li",{children:e},e))}):null]})]})]}):null,(null==eJ?void 0:null===(Q=eJ.stack)||void 0===Q?void 0:Q.neko_login)?(0,n.jsxs)("div",{className:"rounded-2xl border border-fuchsia-500/25 bg-gradient-to-r from-fuchsia-500/[0.08] to-violet-500/[0.06] p-5",children:[(0,n.jsxs)("h3",{className:"text-sm font-semibold text-white",children:["Neko ",tC("登录","sign-in")]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:eJ.stack.neko_login.note_zh||tC(tI?"Neko Chromium ".concat(tI," 使用下列账号策略。"):"Neko Chromium 多实例使用下列账号策略。",tI?"Neko Chromium ".concat(tI," uses the policy below."):"Neko Chromium instances use the policy below.")}),(0,n.jsx)("p",{className:"mt-2 text-[11px] text-amber-200/85",children:tC("重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678)。用错密码会像一直加载;勿把「用户名/密码」写在一行。","Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.")}),tE.length?(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:tE.map(e=>(0,n.jsxs)("a",{href:e.url,target:"_blank",rel:"noreferrer",className:"rounded-lg border border-fuchsia-400/20 bg-black/25 px-3 py-2 text-[11px] text-fuchsia-100 ring-1 ring-fuchsia-400/20",children:[e.label,e.port?" :".concat(e.port):""]},e.key))}):null,(0,n.jsxs)("div",{className:"mt-4 grid gap-3 sm:grid-cols-2 font-mono text-sm",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/40 px-4 py-3",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase text-zinc-500",children:tC("昵称示例","Display Name Example")}),(0,n.jsx)("p",{className:"mt-1 text-fuchsia-200",children:null!==(eT=eJ.stack.neko_login.user_login)&&void 0!==eT?eT:"live"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] uppercase text-zinc-500",children:tC("连接口令","Access Password")}),(0,n.jsx)("p",{className:"mt-0.5 text-fuchsia-100",children:null!==(eU=eJ.stack.neko_login.user_password)&&void 0!==eU?eU:"—"})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/40 px-4 py-3",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase text-zinc-500",children:tC("管理昵称示例","Admin Display Name Example")}),(0,n.jsx)("p",{className:"mt-1 text-violet-200",children:null!==(eE=eJ.stack.neko_login.admin_login)&&void 0!==eE?eE:"admin"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] uppercase text-zinc-500",children:tC("管理口令","Admin Password")}),(0,n.jsx)("p",{className:"mt-0.5 text-violet-100",children:null!==(eI=eJ.stack.neko_login.admin_password)&&void 0!==eI?eI:"—"})]})]}),(0,n.jsx)("p",{className:"mt-3 text-[10px] text-zinc-600",children:tC("x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;默认保留管理员会话、开发者工具和 Tampermonkey 持久化。本栈未启用 Firefox 版 Neko。","x86 defaults to Neko’s Chrome image, ARM defaults to Chromium, and admin access, DevTools, plus persistent Tampermonkey stay enabled. This stack does not use Firefox Neko.")})]}):null,(null==eJ?void 0:null===(et=eJ.network)||void 0===et?void 0:null===(ee=et.interfaces)||void 0===ee?void 0:ee.length)?(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-gradient-to-br from-cyan-500/[0.07] to-transparent p-5",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(k.Z,{className:"h-4 w-4 text-cyan-300"}),tC("网络带宽 / 流量","Network bandwidth / traffic")]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:tC("实时速率由总览自动刷新间隔计算;累计值来自系统网卡计数器。","Live rates are calculated from dashboard refresh deltas; totals come from kernel counters.")}),(0,n.jsxs)("div",{className:"mt-4 grid gap-3 sm:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase tracking-[0.08em] text-cyan-300/70",children:tC("实时下行","Live down")}),(0,n.jsx)("p",{className:"mt-1 text-lg font-semibold text-cyan-100",children:e3(eJ.network.total_rx_bps)}),(0,n.jsxs)("p",{className:"mt-1 text-zinc-500",children:[tC("累计","Total"),": ",e2(eJ.network.total_rx_bytes)]})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase tracking-[0.08em] text-cyan-300/70",children:tC("实时上行","Live up")}),(0,n.jsx)("p",{className:"mt-1 text-lg font-semibold text-cyan-100",children:e3(eJ.network.total_tx_bps)}),(0,n.jsxs)("p",{className:"mt-1 text-zinc-500",children:[tC("累计","Total"),": ",e2(eJ.network.total_tx_bytes)]})]})]}),(0,n.jsx)("div",{className:"mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3",children:(eJ.network.interfaces||[]).slice(0,6).map(e=>{var t,s,l,i;return(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-cyan-200/90",children:e.name}),(0,n.jsxs)("p",{className:"mt-2 text-zinc-500",children:[tC("链路","Link"),": ",e.operstate||"unknown"," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",e.speed_mbps?"".concat(e.speed_mbps," Mbps"):tC("未知速率","unknown speed")]}),(0,n.jsxs)("p",{className:"mt-2 text-zinc-500",children:["↓ ",e3(e.rx_bps)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",e2(e.rx_bytes)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",null!==(l=null===(t=e.rx_packets)||void 0===t?void 0:t.toLocaleString())&&void 0!==l?l:"—"," pkt"]}),(0,n.jsxs)("p",{className:"mt-0.5 text-zinc-500",children:["↑ ",e3(e.tx_bps)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",e2(e.tx_bytes)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",null!==(i=null===(s=e.tx_packets)||void 0===s?void 0:s.toLocaleString())&&void 0!==i?i:"—"," pkt"]})]},e.name)})})]}):null,(null==eJ?void 0:null===(en=eJ.live_events)||void 0===en?void 0:null===(es=en.pm2_tail)||void 0===es?void 0:es.length)||(null==eJ?void 0:null===(ei=eJ.live_events)||void 0===ei?void 0:null===(el=ei.process_digest)||void 0===el?void 0:el.length)?(0,n.jsxs)("div",{className:"grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/15 bg-zinc-950/40 p-5",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:tC("直播 / PM2 事件","Live / PM2 feed")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:tC("PM2 合并日志尾部","PM2 merged log tail")}),(0,n.jsx)("pre",{className:"mt-3 max-h-48 overflow-auto whitespace-pre-wrap rounded-lg border border-white/[0.05] bg-black/40 p-3 font-mono text-[10px] leading-relaxed text-zinc-400",children:((null==eJ?void 0:null===(ea=eJ.live_events)||void 0===ea?void 0:ea.pm2_tail)||[]).join("\n")||tC("暂无 PM2 输出","No PM2 output")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/15 bg-zinc-950/40 p-5",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:tC("进程快照","Process digest")}),(0,n.jsx)("ul",{className:"mt-3 space-y-2",children:((null==eJ?void 0:null===(eo=eJ.live_events)||void 0===eo?void 0:eo.process_digest)||[]).map(e=>(0,n.jsxs)("li",{className:"flex items-center justify-between gap-2 rounded-lg border border-white/[0.05] bg-black/30 px-3 py-2 text-xs",children:[(0,n.jsx)("span",{className:"truncate text-zinc-300",children:e.label||e.pm2}),(0,n.jsx)("span",{className:"shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ".concat(e4(e.status||"")),children:e6(e.status||"",tC)})]},e.pm2))})]})]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/20 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/50 to-fuchsia-500/[0.06] p-5 shadow-lg shadow-violet-500/5",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:tC("直播进程","Live processes")}),(0,n.jsx)("span",{className:"rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-300",children:(null==eJ?void 0:null===(ec=eJ.live)||void 0===ec?void 0:ec.backend_mode)||"—"})]}),(0,n.jsxs)("ul",{className:"mt-4 space-y-2.5",children:[((null==eJ?void 0:null===(ed=eJ.live)||void 0===ed?void 0:ed.processes)||[]).slice(0,10).map(e=>(0,n.jsxs)("li",{className:"group flex items-center gap-3 rounded-xl border border-white/[0.08] bg-black/35 px-3 py-2.5 transition hover:border-violet-400/25 hover:bg-black/45",children:[(0,n.jsx)("div",{className:"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/30 to-fuchsia-500/20 text-[10px] font-bold text-violet-100 ring-1 ring-white/10",children:"LIVE"}),(0,n.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,n.jsx)("p",{className:"truncate text-sm font-medium text-zinc-100",children:e.label||e.pm2}),(0,n.jsx)("p",{className:"truncate font-mono text-[10px] text-zinc-600",children:e.script||e.pm2})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-lg px-2.5 py-1 text-[10px] font-semibold ".concat(e4(e.process_status||"")),children:e6(e.process_status||"",tC)})]},e.pm2)),(null==eJ?void 0:null===(em=eJ.live)||void 0===em?void 0:null===(eu=em.processes)||void 0===eu?void 0:eu.length)?null:(0,n.jsx)("li",{className:"rounded-xl border border-dashed border-white/10 py-8 text-center text-xs text-zinc-600",children:tC("暂无直播进程","No live processes")})]}),(0,n.jsxs)("div",{className:"mt-5 grid grid-cols-2 gap-2",children:[(0,n.jsx)("button",{type:"button",onClick:()=>eF("live_youtube"),className:"rounded-xl border border-violet-400/35 bg-violet-500/15 py-2.5 text-[11px] font-semibold text-violet-100 shadow-inner shadow-black/20 transition hover:bg-violet-500/25",children:"YouTube"}),(0,n.jsx)("button",{type:"button",onClick:()=>eF("live_tiktok"),className:"rounded-xl border border-cyan-400/35 bg-cyan-500/15 py-2.5 text-[11px] font-semibold text-cyan-100 shadow-inner shadow-black/20 transition hover:bg-cyan-500/25",children:"TikTok"})]})]}),(null==eJ?void 0:eJ.probes)&&tw?(0,n.jsxs)("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-3",children:[eJ.probes.srs?(()=>{var e,t,s,l,i,r,a;let o=null===(e=eJ.probes)||void 0===e?void 0:e.srs,c=null==o?void 0:null===(t=o.tcp)||void 0===t?void 0:t.reachable,d=null==o?void 0:null===(s=o.tcp_api)||void 0===s?void 0:s.reachable,x=null==o?void 0:o.http_ui,u=null==o?void 0:o.http,m=null==o?void 0:o.api_port,h=null!==(i=null==o?void 0:o.api_ok)&&void 0!==i?i:(null==u?void 0:u.http_code)===200,p=null==x?void 0:x.http_code;return(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-zinc-950/40 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-white",children:"SRS"}),(0,n.jsx)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat(h?"bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-1 ring-rose-500/25"),children:h?tC("API 正常","API OK"):tC("API 异常","API down")})]}),(0,n.jsxs)("p",{className:"mt-1 font-mono text-[11px] text-zinc-500",children:[":",null!==(r=null==o?void 0:o.port)&&void 0!==r?r:"—"," / API :",null!=m?m:"—"," \xb7 TCP ",c?"OK":"—"," \xb7 API TCP"," ",d?"OK":"—"]}),(0,n.jsxs)("p",{className:"mt-1 font-mono text-[11px] text-zinc-500",children:["API HTTP ",null!==(a=null==u?void 0:u.http_code)&&void 0!==a?a:"—"," \xb7 ",tC("根路径","root")," ",null!=p?p:"—",404===p?tC("(无首页不影响推流)"," (no index is OK for streaming)"):null]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[tC("延迟","RTT")," ",(null==u?void 0:u.latency_ms)!=null?"".concat(u.latency_ms," ms"):(null==o?void 0:null===(l=o.tcp)||void 0===l?void 0:l.latency_ms)!=null?"".concat(o.tcp.latency_ms," ms"):"—"]}),!h&&(null==u?void 0:u.error)?(0,n.jsx)("p",{className:"mt-1 line-clamp-2 text-[10px] text-amber-200/80",children:u.error}):null]})})():null,eJ.probes.neko?(()=>{var e,t,s,l,i,r,a;let o=null===(e=eJ.probes)||void 0===e?void 0:e.neko,c=null==o?void 0:null===(t=o.tcp)||void 0===t?void 0:t.reachable,d=null==o?void 0:o.http,x=(null==o?void 0:o.instances)||[],u=null!==(l=null==o?void 0:o.running_count)&&void 0!==l?l:x.filter(e=>{var t;return null===(t=e.tcp)||void 0===t?void 0:t.reachable}).length,m=null!==(i=null==o?void 0:o.required_count)&&void 0!==i?i:x.length;return(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-zinc-950/40 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-white",children:"Neko Chromium"}),(0,n.jsxs)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat(u>0?"bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-1 ring-rose-500/25"),children:[u,"/",m||"—"," ",tC("在线","online")]})]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:tC("Docker 内 Chromium(非 Chrome 品牌);arm64/x86 同一镜像。","Chromium in Docker (not Chrome branding); multi-arch.")}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-[11px] text-zinc-500",children:[":",null!==(r=null==o?void 0:o.port)&&void 0!==r?r:"—"," \xb7 TCP ",c?"OK":"—"," \xb7 HTTP ",null!==(a=null==d?void 0:d.http_code)&&void 0!==a?a:"—"]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[tC("延迟","RTT")," ",(null==d?void 0:d.latency_ms)!=null?"".concat(d.latency_ms," ms"):(null==o?void 0:null===(s=o.tcp)||void 0===s?void 0:s.latency_ms)!=null?"".concat(o.tcp.latency_ms," ms"):"—"]}),x.length?(0,n.jsx)("div",{className:"mt-3 space-y-2",children:x.map(e=>{var t,s,l,i,r,a,o;return(0,n.jsxs)("div",{className:"flex items-center justify-between gap-3 rounded-lg border border-white/[0.05] bg-black/25 px-3 py-2",children:[(0,n.jsxs)("div",{className:"min-w-0",children:[(0,n.jsxs)("p",{className:"text-[11px] font-semibold text-zinc-200",children:["Neko ",null!==(r=e.id)&&void 0!==r?r:"?"]}),(0,n.jsxs)("p",{className:"font-mono text-[10px] text-zinc-500",children:[":",null!==(a=e.port)&&void 0!==a?a:"—"," \xb7 TCP ",(null===(t=e.tcp)||void 0===t?void 0:t.reachable)?"OK":"—"," \xb7 HTTP"," ",null!==(o=null===(s=e.http)||void 0===s?void 0:s.http_code)&&void 0!==o?o:"—"]})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat((null===(l=e.tcp)||void 0===l?void 0:l.reachable)?"bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-500/25":"bg-zinc-500/15 text-zinc-400 ring-1 ring-zinc-500/25"),children:(null===(i=e.tcp)||void 0===i?void 0:i.reachable)?tC("运行中","Running"):tC("未连通","Offline")})]},"".concat(e.id,"-").concat(e.port))})}):null,!(null==d?void 0:d.reachable)&&(null==d?void 0:d.error)?(0,n.jsx)("p",{className:"mt-1 line-clamp-2 text-[10px] text-amber-200/80",children:d.error}):null]})})():null]}):null]}):null,"services"===eK?(0,n.jsxs)("div",{className:"grid gap-4 md:grid-cols-2 xl:grid-cols-3",children:[((null==eJ?void 0:eJ.services)||[]).map((e,t)=>(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},transition:{delay:.04*t},whileHover:{y:-2},className:"rounded-2xl border border-white/[0.07] bg-gradient-to-b from-white/[0.05] to-transparent p-5",children:[(0,n.jsxs)("div",{className:"flex items-start justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"font-semibold text-white",children:e.label}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500 line-clamp-2",children:e.description})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-full px-2.5 py-1 text-[10px] font-semibold ".concat(e4(e.running?"online":e.status)),children:e.running?tC("运行中","Running"):e.status})]}),e.detail?(0,n.jsx)("p",{className:"mt-2 text-[11px] leading-snug line-clamp-6 ".concat("skipped"===e.status?"text-slate-300/90":"text-amber-200/85"),children:e.detail}):null,"shellcrash"===e.service_id&&e.available&&!e.running?(0,n.jsx)("p",{className:"mt-2 text-[11px] text-zinc-500",children:tC("stopped 为常态:仅当需要透明代理 / 规则分流时再点 Start(或 systemctl enable --now shellcrash)。","Stopped is normal; Start only when you need the proxy (or enable shellcrash.service).")}):null,(0,n.jsxs)("div",{className:"mt-4 flex flex-wrap items-center gap-2",children:[e.url?(0,n.jsx)("a",{href:(0,ex.p)(e.url),target:"_blank",rel:"noreferrer",className:"rounded-lg bg-violet-500/15 px-3 py-1.5 text-xs font-medium text-violet-200 ring-1 ring-violet-500/25",children:tC("打开面板","Open panel")}):null,(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"start"),className:"rounded-lg bg-emerald-500/20 px-3 py-1.5 text-xs font-medium text-emerald-200 ring-1 ring-emerald-500/30 disabled:opacity-30",children:"Start"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"install"),className:"rounded-lg bg-sky-500/15 px-3 py-1.5 text-xs font-medium text-sky-200 ring-1 ring-sky-500/25 disabled:opacity-30",children:"Install"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"restart"),className:"rounded-lg bg-amber-500/15 px-3 py-1.5 text-xs font-medium text-amber-200 ring-1 ring-amber-500/25 disabled:opacity-30",children:"Restart"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"upgrade"),className:"rounded-lg bg-indigo-500/15 px-3 py-1.5 text-xs font-medium text-indigo-200 ring-1 ring-indigo-500/25 disabled:opacity-30",children:"Upgrade"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"stop"),className:"rounded-lg bg-rose-500/15 px-3 py-1.5 text-xs font-medium text-rose-200 ring-1 ring-rose-500/25 disabled:opacity-30",children:"Stop"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?tC("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||tC("当前不可操作","Unavailable"),onClick:()=>void tD(e.service_id,"uninstall"),className:"rounded-lg bg-zinc-500/15 px-3 py-1.5 text-xs font-medium text-zinc-200 ring-1 ring-zinc-500/25 disabled:opacity-30",children:"Uninstall"})]})]},e.service_id)),(null==eJ?void 0:null===(eh=eJ.services)||void 0===eh?void 0:eh.length)?null:(0,n.jsx)("p",{className:"text-sm text-zinc-500",children:tC("暂无服务数据","No services")})]}):null,"live_youtube"===eK?(0,n.jsx)(e0,{t:tC,busy:eQ,entries:tn,currentProc:ta,setCurrentProc:to,urlConfig:tc,setUrlConfig:td,onRunProcess:(e,t)=>void tA(e,t),onSaveUrlConfig:(e,t)=>tZ(e,t),fetchJson:tP,liveProcesses:ti.length?ti:null!==(eD=null==eJ?void 0:null===(ep=eJ.live)||void 0===ep?void 0:ep.processes)&&void 0!==eD?eD:[],notify:tR,quickLinks:tU,onNavigate:e=>eF(e)}):null,"live_tiktok"===eK?(0,n.jsx)(ez,{t:tC,busy:!!eQ,entries:tn,currentProc:ta,setCurrentProc:to,urlConfig:tc,setUrlConfig:td,onRunProcess:e=>void tA(e),onSaveUrlConfig:e=>tZ(e),onReplayGoLive:async(e,t)=>{let s=t.replace(/[,,|]/g,"_");await tZ("原画,".concat(e,",主播: ").concat(s)),await tA("start")},onCopy:tB,fetchJson:tP,apiPrefix:eA,quickLinks:tU,hdmiStatus:null==eJ?void 0:eJ.hdmi,notify:tR,onNavigate:e=>eF(e)}):null,"android"===eK?(0,n.jsx)(er,{t:tC,notify:tR,fetchJson:tP,apiBase:eA,busy:eQ,androidSerial:tm,setAndroidSerial:th,devices:(null==eJ?void 0:null===(eb=eJ.android)||void 0===eb?void 0:eb.devices)||[],adbAvailable:!!(null==eJ?void 0:null===(eg=eJ.android)||void 0===eg?void 0:eg.adb_available),apkPath:tx,setApkPath:tu,onAndroidAction:(e,t)=>tO(e,null!=t?t:{}),onCopy:tB,onRefreshDevices:()=>void tL()}):null,"network"===eK?(0,n.jsxs)("div",{className:"mx-auto max-w-7xl space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border p-6 ".concat("light"===eH?"border-violet-200/90 bg-gradient-to-br from-violet-50 via-white to-fuchsia-50/80":"border-violet-500/25 bg-gradient-to-br from-violet-500/10 via-zinc-950/40 to-fuchsia-500/5"),children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold ".concat("light"===eH?"text-slate-900":"text-white"),children:[(0,n.jsx)(k.Z,{className:"h-4 w-4 ".concat("light"===eH?"text-violet-600":"text-violet-300")}),tC("ShellCrash 网络面板","ShellCrash panel")]}),(0,n.jsx)("p",{className:"mt-2 text-xs ".concat("light"===eH?"text-slate-600":"text-zinc-400"),children:tC("透明代理与规则分流直接在本页完成,无需跳转;流媒体与其它系统服务请在「服务」页管理。","Proxy rules and YAML editing are embedded here. Use Services for SRS/Neko/Chromium, etc.")}),(0,n.jsx)("div",{className:"mt-6 flex flex-col items-center gap-4 sm:flex-row sm:justify-center",children:[{tx:tC("互联网","Internet"),c:"from-cyan-400 to-blue-500"},{tx:"ShellCrash",c:"from-violet-500 to-fuchsia-500"},{tx:tC("本机 / 安卓","Host / Android"),c:"from-emerald-400 to-teal-500"}].map((e,t)=>(0,n.jsxs)("div",{className:"flex items-center gap-4",children:[(0,n.jsx)("div",{className:"flex h-16 w-28 items-center justify-center rounded-2xl bg-gradient-to-br ".concat(e.c," text-xs font-bold text-white shadow-lg"),children:e.tx}),t<2?(0,n.jsx)(h.Z,{className:"hidden h-5 w-5 sm:block ".concat("light"===eH?"text-slate-400":"text-zinc-600")}):null]},e.tx))})]}),(0,n.jsx)("div",{className:"overflow-hidden rounded-2xl border shadow-xl ".concat("light"===eH?"border-slate-200/90 bg-white shadow-slate-200/40":"border-white/[0.08] bg-zinc-950/50 shadow-black/30"),children:(0,n.jsx)("div",{className:"max-h-[min(78vh,56rem)] min-h-[22rem] overflow-y-auto overflow-x-hidden",children:(0,n.jsx)(P.d,{embeddedShellCrash:!0,portalLang:eZ})})})]}):null,"settings"===eK?(0,n.jsxs)("div",{className:"mx-auto max-w-lg space-y-6",children:[(0,n.jsx)("p",{className:"text-sm ".concat("light"===eH?"text-slate-600":"text-zinc-400"),children:tC("常用选项(本机偏好);完整文件与 JSON 编辑见下方链接。","Local preferences; full file/JSON editor via the link below.")}),(0,n.jsxs)("div",{className:"space-y-4 rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsxs)("label",{className:"flex cursor-pointer items-center justify-between gap-4",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-sm font-medium text-white",children:tC("总览自动刷新","Auto-refresh dashboard")}),(0,n.jsx)("p",{className:"text-[11px] text-zinc-500",children:"10s"})]}),(0,n.jsx)("input",{type:"checkbox",className:"h-5 w-5 accent-violet-500",checked:tv,onChange:e=>{let t=e.target.checked;ty(t);try{localStorage.setItem("livehub.autorefresh",t?"1":"0")}catch(e){}tR(t?tC("已开启刷新","Auto refresh on"):tC("已暂停刷新","Paused"))}})]}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center justify-between gap-4 border-t border-white/[0.06] pt-4",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-sm font-medium text-white",children:tC("总览显示 SRS / Neko 探针","Show SRS/Neko probes")}),(0,n.jsx)("p",{className:"text-[11px] text-zinc-500",children:tC("关闭可精简首页","Hide stream probe cards")})]}),(0,n.jsx)("input",{type:"checkbox",className:"h-5 w-5 accent-violet-500",checked:tw,onChange:e=>{let t=e.target.checked;tN(t);try{localStorage.setItem("livehub.showStreamProbes",t?"1":"0")}catch(e){}}})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/40 p-5",children:[(0,n.jsx)("p",{className:"text-xs font-semibold uppercase tracking-wider text-zinc-500",children:tC("配置快照","Config snapshot")}),(0,n.jsx)("p",{className:"mt-2 font-mono text-[11px] text-zinc-400",children:String((null==tp?void 0:tp.config_root)||"—")}),(0,n.jsxs)("p",{className:"mt-3 text-sm text-zinc-300",children:[tC("主机名","Hostname"),":"," ",(0,n.jsx)("span",{className:"font-mono text-violet-200",children:(()=>{var e,t,s;let n=null==tp?void 0:tp.system;return(null==n?void 0:null===(t=n.identity)||void 0===t?void 0:null===(e=t.hostname_alias)||void 0===e?void 0:e.trim())||((null==eJ?void 0:null===(s=eJ.stack)||void 0===s?void 0:s.mdns_host)||"").replace(/\.local$/i,"")||"—"})()})]}),(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:null==tp?void 0:null===(ef=tp.services)||void 0===ef?void 0:ef.slice(0,8).map(e=>(0,n.jsxs)("span",{className:"rounded-full px-2.5 py-1 text-[10px] font-medium ".concat(e.enabled?"bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-500/25":"bg-zinc-500/15 text-zinc-400"),children:[e.id," ",e.enabled?"ON":"off"]},e.id))})]}),(0,n.jsxs)("a",{href:"/console?tab=config",className:"inline-flex w-full items-center justify-center gap-2 rounded-xl border py-3 text-sm transition ".concat("light"===eH?"border-slate-200 bg-slate-50 text-violet-700 hover:bg-slate-100":"border-white/10 bg-white/[0.05] text-violet-200 hover:bg-white/[0.08]"),children:[tC("完整配置中心(文件 / JSON)","Full config center (files / JSON)"),(0,n.jsx)(z.Z,{className:"h-4 w-4"})]})]}):null]},eK)})})]})]}),(0,n.jsx)(i.M,{children:eW?(0,n.jsx)(l.E.div,{initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},className:"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm lg:hidden",onClick:()=>eY(!1),children:(0,n.jsxs)(l.E.aside,{initial:{x:-280},animate:{x:0},exit:{x:-280},transition:{type:"spring",damping:28,stiffness:320},className:"absolute left-0 top-0 h-full w-[min(88vw,280px)] border-r border-white/10 bg-zinc-950 p-4 shadow-2xl",onClick:e=>e.stopPropagation(),children:[(0,n.jsx)("div",{className:"flex justify-end",children:(0,n.jsx)("button",{type:"button",onClick:()=>eY(!1),className:"rounded-lg p-2",children:(0,n.jsx)(S.Z,{className:"h-5 w-5"})})}),(0,n.jsx)("nav",{className:"mt-4 flex flex-col gap-1",children:tH.map(e=>{let t=e.icon,s=eK===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>{eF(e.id),eY(!1)},"aria-current":s?"page":void 0,className:"flex items-center gap-3 rounded-xl px-3 py-3 text-left text-sm transition ".concat(s?"bg-white/[0.08] text-white shadow-inner shadow-white/5":"text-zinc-300 hover:bg-white/[0.04]"),children:[(0,n.jsx)(t,{className:"h-4 w-4 ".concat(s?"text-violet-300":"text-violet-400")}),tC(e.labelZh,e.labelEn)]},e.id)})})]})}):null})]})}}},function(e){e.O(0,[500,719,246,971,117,744],function(){return e(e.s=4578)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/d2ypp2/web-console/out/_next/static/chunks/app/page-2e4163a91e299843.js b/d2ypp2/web-console/out/_next/static/chunks/app/page-2e4163a91e299843.js deleted file mode 100644 index 4fddd82..0000000 --- a/d2ypp2/web-console/out/_next/static/chunks/app/page-2e4163a91e299843.js +++ /dev/null @@ -1 +0,0 @@ -(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[931],{4578:function(e,t,s){Promise.resolve().then(s.bind(s,1499))},1499:function(e,t,s){"use strict";s.r(t),s.d(t,{default:function(){return e6}});var n=s(7437),l=s(6247),i=s(7586),r=s(4401),a=s(6137),o=s(4372),c=s(5901),d=s(3388),x=s(3420),u=s(8728),m=s(2023),h=s(407),p=s(6840),b=s(7226),g=s(5929),f=s(7945),v=s(1817),y=s(7168),w=s(4822),N=s(4428),j=s(1723),k=s(2926),z=s(7580),S=s(2489),_=s(2265);function C(e){let{tr:t}=e,l=(0,_.useRef)(null);return(0,_.useEffect)(()=>{let e;if(!l.current)return;let t=!1,n=0;return s.e(689).then(s.bind(s,1448)).then(s=>{if(t||!l.current)return;let i=l.current,r=Math.max(i.clientWidth,280),a=Math.max(i.clientHeight,200),o=new s.Scene,c=new s.PerspectiveCamera(48,r/a,.1,80);c.position.set(0,1.1,7.2);let d=new s.WebGLRenderer({antialias:!0,alpha:!0});d.setPixelRatio(Math.min(window.devicePixelRatio,2)),d.setSize(r,a),d.setClearColor(0,0),i.appendChild(d.domElement);let x=new s.HemisphereLight(16762092,4482815,.85);o.add(x);let u=new s.DirectionalLight(16777215,.9);u.position.set(4,8,6),o.add(u);let m=new s.PointLight(8189439,.6,20);m.position.set(-3,2,-2),o.add(m);let h=new s.Group,p=new s.Mesh(new s.BoxGeometry(3.2,.35,2.2),new s.MeshStandardMaterial({color:2762048,metalness:.15,roughness:.55}));p.position.y=-.85,h.add(p);let b=new s.Mesh(new s.TorusGeometry(.72,.09,12,48),new s.MeshStandardMaterial({color:16724821,emissive:4456465,metalness:.35,roughness:.25}));b.rotation.x=Math.PI/2,b.position.set(-.85,.15,.2),h.add(b);let g=new s.Mesh(new s.ConeGeometry(.35,.5,3),new s.MeshStandardMaterial({color:16777215,emissive:2236962,metalness:.2,roughness:.3}));g.rotation.z=-Math.PI/2,g.rotation.y=Math.PI,g.position.set(-.55,.18,.2),h.add(g);let f=new s.Mesh(new s.CylinderGeometry(.22,.28,.95,20),new s.MeshStandardMaterial({color:2487534,emissive:340024,metalness:.25,roughness:.35}));f.position.set(1.05,.35,-.15),f.rotation.z=.12,h.add(f);let v=new s.Mesh(new s.SphereGeometry(.26,16,16),new s.MeshStandardMaterial({color:16657493,emissive:2229520,metalness:.2,roughness:.4}));v.position.set(1.05,.92,-.15),h.add(v);let y=new s.BufferGeometry,w=new Float32Array(360);for(let e=0;e<120;e++)w[3*e]=(Math.random()-.5)*14,w[3*e+1]=(Math.random()-.3)*8,w[3*e+2]=(Math.random()-.5)*8-2;y.setAttribute("position",new s.BufferAttribute(w,3));let N=new s.Points(y,new s.PointsMaterial({color:16777215,size:.06,transparent:!0,opacity:.7}));o.add(N),o.add(h);let j=performance.now(),k=()=>{if(t)return;n=requestAnimationFrame(k);let e=(performance.now()-j)/1e3;h.rotation.y=.25*Math.sin(.35*e)+.12*e,h.position.y=.06*Math.sin(.9*e),b.rotation.z=.4*e,N.rotation.y=.02*e,d.render(o,c)};k();let z=new ResizeObserver(()=>{if(!l.current||t)return;let e=Math.max(l.current.clientWidth,280),s=Math.max(l.current.clientHeight,200);c.aspect=e/s,c.updateProjectionMatrix(),d.setSize(e,s)});z.observe(i),e=()=>{cancelAnimationFrame(n),z.disconnect(),d.dispose(),y.dispose(),d.domElement.parentNode===i&&i.removeChild(d.domElement)}}),()=>{t=!0,cancelAnimationFrame(n),null==e||e()}},[]),(0,n.jsxs)("div",{className:"flex h-full min-h-[14rem] flex-col",children:[(0,n.jsx)("p",{className:"mb-2 px-1 text-[10px] font-medium uppercase tracking-wider text-fuchsia-200/80",children:t("直播工作室 \xb7 3D","Live studio \xb7 3D")}),(0,n.jsx)("div",{ref:l,className:"relative min-h-[12rem] flex-1 overflow-hidden rounded-xl bg-gradient-to-b from-fuchsia-950/40 to-cyan-950/20 ring-1 ring-white/10"})]})}var R=s(9170),P=s(2246),L=s(4286),M=s(4794),T=s(221),U=s(8867),E=s(8124),I=s(2101),A=s(3651),D=s(9985),Z=s(6362),O=s(3468),H=s(6443),B=s(5287),K=s(3276),F=s(9227),W=s(257),Y=s(1387);class J{reset(){if(this.buf=new Uint8Array(0),this.sps=null,this.pps=null,this.configured=!1,this.configuring=!1,this.ts=0,this.decoder){try{this.decoder.close()}catch(e){}this.decoder=null}}push(e){let t=new Uint8Array(e.byteLength);t.set(new Uint8Array(e)),this.buf=function(e,t){let s=new Uint8Array(e.length+t.length);return s.set(e,0),s.set(t,e.length),s}(this.buf,t),this.drainAvcc()}drainAvcc(){let e=this.buf,t=0;for(;t+4<=e.length;){var s;let n=e[s=t]<<24|e[s+1]<<16|e[s+2]<<8|e[s+3];if(n<=0||n>6291456){this.buf=new Uint8Array(e.subarray(t+1)),t=0;continue}if(t+4+n>e.length)break;let l=e.subarray(t+4,t+4+n);t+=4+n,this.handleNal(l)}t>0&&(this.buf=new Uint8Array(e.subarray(t)))}handleNal(e){if(e.length<2)return;let t=31&e[0];if(7===t?this.sps=new Uint8Array(e):8===t&&(this.pps=new Uint8Array(e)),this.sps&&this.pps&&!this.configured&&!this.configuring&&this.configure(),!this.configured||!this.decoder||1!==t&&5!==t&&7!==t&&8!==t||7===t||8===t)return;let s=new Uint8Array(4+e.length);new DataView(s.buffer).setUint32(0,e.length,!1),s.set(e,4);try{let e=new EncodedVideoChunk({type:5===t?"key":"delta",timestamp:this.ts,data:s});this.ts+=33333,this.decoder.decode(e)}catch(e){this.onDecodeError(e.message||String(e))}}async configure(){if(this.sps&&this.pps&&!this.configured&&!this.configuring){this.configuring=!0;try{let e=function(e,t){let s=e.subarray(1),n=t.subarray(1),l=new Uint8Array(11+s.length+3+n.length),i=0;return l[i++]=1,l[i++]=e[1],l[i++]=e[2],l[i++]=e[3],l[i++]=255,l[i++]=225,l[i++]=s.length>>8&255,l[i++]=255&s.length,l.set(s,i),i+=s.length,l[i++]=1,l[i++]=n.length>>8&255,l[i++]=255&n.length,l.set(n,i),l}(this.sps,this.pps),t=function(e){let t=e=>e.toString(16).padStart(2,"0").toUpperCase();return"avc1.".concat(t(e[1])).concat(t(e[2])).concat(t(e[3]))}(this.sps),s=new VideoDecoder({output:e=>{let t=this.canvas.getContext("2d");if(!t){e.close();return}(this.canvas.width!==e.displayWidth||this.canvas.height!==e.displayHeight)&&(this.canvas.width=e.displayWidth,this.canvas.height=e.displayHeight),t.drawImage(e,0,0),e.close()},error:e=>this.onDecodeError(String(e.message||e))});await s.configure({codec:t,description:e,optimizeForLatency:!0}),this.decoder=s,this.configured=!0}catch(e){this.onDecodeError(e.message||String(e))}finally{this.configuring=!1}}}constructor(e,t){this.canvas=e,this.onDecodeError=t,this.buf=new Uint8Array(0),this.decoder=null,this.configured=!1,this.configuring=!1,this.ts=0,this.sps=null,this.pps=null}}function G(){return"undefined"!=typeof VideoDecoder&&"undefined"!=typeof VideoFrame}let V="live-hub-adb-bookmarks-v1";function q(e,t,s){let n,l,i,r;let a=e.getBoundingClientRect(),o=e.width,c=e.height;if(!o||!c)return null;let d=a.width/a.height,x=o/c;x>d?(n=a.width,l=a.width/x,i=0,r=(a.height-l)/2):(l=a.height,n=a.height*x,i=(a.width-n)/2,r=0);let u=t-a.left-i,m=s-a.top-r;return u<0||m<0||u>n||m>l?null:{x:Math.max(0,Math.min(o-1,Math.round(u/n*o))),y:Math.max(0,Math.min(c-1,Math.round(m/l*c)))}}function X(e,t,s){let n,l,i,r;let a=e.getBoundingClientRect(),o=e.naturalWidth,c=e.naturalHeight;if(!o||!c)return null;let d=a.width/a.height,x=o/c;x>d?(n=a.width,l=a.width/x,i=0,r=(a.height-l)/2):(l=a.height,n=a.height*x,i=(a.width-n)/2,r=0);let u=t-a.left-i,m=s-a.top-r;return u<0||m<0||u>n||m>l?null:{x:Math.max(0,Math.min(o-1,Math.round(u/n*o))),y:Math.max(0,Math.min(c-1,Math.round(m/l*c)))}}let $=[0,280,450,700,1100];function Q(e){switch(e){case"usb":return{Icon:I.Z,zh:"USB",en:"USB",cls:"bg-amber-500/20 text-amber-100 ring-amber-500/35"};case"tcp":return{Icon:k.Z,zh:"网络",en:"TCP",cls:"bg-cyan-500/20 text-cyan-100 ring-cyan-500/35"};case"emulator":return{Icon:A.Z,zh:"模拟器",en:"Emu",cls:"bg-fuchsia-500/20 text-fuchsia-100 ring-fuchsia-500/35"};default:return{Icon:d.Z,zh:"?",en:"?",cls:"bg-zinc-600/30 text-zinc-400"}}}let ee="https://github.com/Genymobile/scrcpy",et="https://github.com/viarotel-org/escrcpy";function es(e,t){let s=t.startsWith("/")?t:"/".concat(t),n=(e||"").trim().replace(/\/$/,"");return n?"".concat(n).concat(s):s}function en(e){var t;let{t:s,apiBase:l,fetchJson:i,serial:r,devices:a,setSerial:o,adbAvailable:c,lock:x,overview:u,onAndroidAction:h,notify:p,onRefreshDevices:b}=e,[g,f]=(0,_.useState)(0),[v,w]=(0,_.useState)(450),[N,j]=(0,_.useState)(!1),[k,z]=(0,_.useState)(null),[S,C]=(0,_.useState)(null),[R,P]=(0,_.useState)(!0),[L,M]=(0,_.useState)(""),[T,U]=(0,_.useState)(""),[E,I]=(0,_.useState)("37777"),[A,en]=(0,_.useState)(""),[el,ei]=(0,_.useState)(!1),[er,ea]=(0,_.useState)([]),[eo,ec]=(0,_.useState)(!1),[ed,ex]=(0,_.useState)(null),[eu,em]=(0,_.useState)(!1),[eh,ep]=(0,_.useState)(null),[eb,eg]=(0,_.useState)(null),[ef,ev]=(0,_.useState)(!0),ey=(0,_.useRef)(null),ew=(0,_.useRef)(null),eN=(0,_.useRef)(null),ej=(0,_.useRef)(null),ek=(0,_.useRef)(null),ez=(0,_.useRef)(null),eS=(0,_.useRef)(0),e_=null===(t=a.find(e=>e.serial===r))||void 0===t?void 0:t.transport,eC=v>0,eR=r?"".concat(es(l,"/android/screenshot"),"?serial=").concat(encodeURIComponent(r),"&ts=").concat(g).concat(eC?"&fast=1":""):"",eP=(0,_.useCallback)(()=>{f(e=>e+1)},[]);(0,_.useEffect)(()=>{let e=()=>ev("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(V);if(e){let t=JSON.parse(e);Array.isArray(t)&&ea(t.map(String).filter(Boolean).slice(0,24))}}catch(e){}},[]);let eL=e=>{let t=e.trim();if(!t)return;let n=[t,...er.filter(e=>e!==t)].slice(0,16);ea(n);try{window.localStorage.setItem(V,JSON.stringify(n))}catch(e){}p(s("已保存快捷地址","Saved quick address"))};(0,_.useEffect)(()=>{if(!r||!c){C(null);return}let e=!1;return(async()=>{try{let t=await (0,Y.N)("".concat(es(l,"/android/display_metrics"),"?serial=").concat(encodeURIComponent(r)));if(e)return;"number"==typeof t.width&&"number"==typeof t.height&&C({width:t.width,height:t.height})}catch(e){}})(),()=>{e=!0}},[l,c,r]),(0,_.useEffect)(()=>{if(!c||!r||v<=0||eo||!ef)return;let e=window.setInterval(()=>f(e=>e+1),v);return()=>clearInterval(e)},[c,v,ef,r,eo]),(0,_.useEffect)(()=>{ef&&c&&r&&!eo&&eP()},[c,eP,ef,r,eo]),(0,_.useEffect)(()=>{if(!c){eg(null);return}let e=!1;return(async()=>{try{let t=await (0,Y.N)(es(l,"/android/scrcpy/info"));e||eg(t)}catch(e){}})(),()=>{e=!0}},[l,c]),(0,_.useEffect)(()=>{eo||ep(null)},[eo]),(0,_.useEffect)(()=>{ex(null)},[r]),(0,_.useEffect)(()=>{let e=a.find(e=>e.serial===r),t=!!(r&&ed===r);if(c&&r&&(null==e?void 0:e.state)==="device"&&(null==eb?void 0:eb.server_jar)&&G()&&!t){ec(!0),w(0);return}if(t||!r||!c||eb&&!eb.server_jar||!G()||e&&"device"!==e.state){ec(!1);return}},[r,a,c,eb,ed]),(0,_.useEffect)(()=>{if(!eo||!r||!c){var e,t;null===(e=ej.current)||void 0===e||e.close(),ej.current=null,null===(t=ek.current)||void 0===t||t.reset(),ek.current=null,em(!1);return}if(!G()){ep(s("当前浏览器不支持 WebCodecs H.264","WebCodecs H.264 not supported in this browser")),ec(!1);return}let n=eN.current;if(!n)return;let i=new J(n,e=>ep(e));ek.current=i;let a=new WebSocket(function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1920,n=(function(e){let t=(e||"").trim();if(t)try{let e=new URL(t,window.location.href),s="https:"===e.protocol?"wss:":"ws:";return"".concat(s,"//").concat(e.host)}catch(e){}let s=void 0!==W&&W.env.NEXT_PUBLIC_API_URL?String(W.env.NEXT_PUBLIC_API_URL).trim():"";if(s)try{let e=new URL(s,window.location.href),t="https:"===e.protocol?"wss:":"ws:";return"".concat(t,"//").concat(e.host)}catch(e){}let n=window.location.port;if("3000"===n||"3001"===n){let e=window.location.hostname||"127.0.0.1";return"ws://".concat(e,":").concat("8001")}let{protocol:l,host:i}=window.location;return"".concat("https:"===l?"wss:":"ws:","//").concat(i)})(e).replace(/\/$/,""),l=new URLSearchParams({serial:t,max_size:String(s)});return"".concat(n,"/android/scrcpy/ws?").concat(l.toString())}(l,r));return ej.current=a,a.binaryType="arraybuffer",a.onopen=()=>{em(!0),ep(null)},a.onmessage=e=>{var t;if("string"==typeof e.data){try{let t=JSON.parse(e.data);t.error&&ep(t.error)}catch(e){}return}null===(t=ek.current)||void 0===t||t.push(e.data)},a.onerror=()=>{ep(s("WebSocket 异常","WebSocket error"))},a.onclose=()=>{var e;em(!1),null===(e=ek.current)||void 0===e||e.reset(),ek.current=null},()=>{a.close(),ej.current=null,i.reset(),ek.current===i&&(ek.current=null)}},[eo,r,c,l,s]),(0,_.useEffect)(()=>{r&&c&&(eS.current=0,f(e=>e+1))},[r,c]);let eM=async()=>{let e=L.trim();if(!e){p(s("填写 host:port","Enter host:port"));return}ei(!0);try{let t=await i("/android/connect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({address:e})});p(t.message||(t.ok?"OK":s("连接结果见日志","See message"))),b(),eP()}catch(e){p(e.message)}finally{ei(!1)}},eT=async e=>{ei(!0);try{let t=await i("/android/disconnect",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({address:e})});p(t.message||"OK"),b()}catch(e){p(e.message)}finally{ei(!1)}},eU=async()=>{let e=T.trim(),t=parseInt(E,10)||37777,n=A.trim();if(!e||!n){p(s("填写配对 IP 与码","Host + code required"));return}ei(!0);try{let l=await i("/android/pair",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({host:e,port:t,code:n})});p(l.message||(l.ok?s("配对成功后再点连接","Paired — now Connect"):"pair"))}catch(e){p(e.message)}finally{ei(!1)}},eE=async e=>{if(!R||x||!c||!r)return;let t=ew.current;if(!t)return;let n=X(t,e.clientX,e.clientY);if(!n)return;let l=Date.now(),i=ey.current;i&&l-i.t<320&&28>Math.abs(i.x-n.x)&&28>Math.abs(i.y-n.y)?(ey.current=null,await h("double_tap",{x:n.x,y:n.y}),p(s("双击","Double tap"))):(ey.current={t:l,x:n.x,y:n.y},await h("tap",{x:n.x,y:n.y})),eP()},eI=async e=>{if(e.preventDefault(),x||!c||!r)return;let t=ew.current;if(!t)return;let n=X(t,e.clientX,e.clientY);n&&(await h("long_press",{x:n.x,y:n.y,duration:720}),p(s("长按","Long press")),eP())},eA=async e=>{if(!R||x||!c||!r)return;let t=eN.current;if(!t)return;let n=q(t,e.clientX,e.clientY);if(!n)return;let l=Date.now(),i=ey.current;i&&l-i.t<320&&28>Math.abs(i.x-n.x)&&28>Math.abs(i.y-n.y)?(ey.current=null,await h("double_tap",{x:n.x,y:n.y}),p(s("双击","Double tap"))):(ey.current={t:l,x:n.x,y:n.y},await h("tap",{x:n.x,y:n.y})),eo||eP()},eD=async e=>{if(e.preventDefault(),x||!c||!r)return;let t=eN.current;if(!t)return;let n=q(t,e.clientX,e.clientY);n&&(await h("long_press",{x:n.x,y:n.y,duration:720}),p(s("长按","Long press")),eo||eP())};if(!c)return(0,n.jsx)("div",{className:"rounded-2xl border border-amber-500/25 bg-amber-500/[0.06] p-6 text-center text-sm text-amber-200/90",children:s("宿主机未安装 adb,无法使用 Live 控制台。","adb is required for the Live console.")});let eZ=Q(e_),eO=eZ.Icon;return(0,n.jsxs)("div",{className:"space-y-5",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-sky-500/20 bg-gradient-to-br from-sky-500/[0.08] via-zinc-950/80 to-zinc-900/90 p-4 ring-1 ring-white/[0.05]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 border-b border-white/[0.06] pb-3",children:[(0,n.jsx)(D.Z,{className:"h-4 w-4 text-sky-400"}),(0,n.jsx)("h4",{className:"text-sm font-semibold text-white",children:s("链路管理","Link manager")}),(0,n.jsx)("span",{className:"text-[10px] text-zinc-500",children:s("USB / Redroid :5555 / 无线调试(先配对再连接)","USB \xb7 Redroid :5555 \xb7 wireless (pair then connect)")})]}),(0,n.jsxs)("div",{className:"mt-3 grid gap-3 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"space-y-2 rounded-xl border border-white/[0.06] bg-black/35 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:"adb connect"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("input",{className:"min-w-[12rem] flex-1 rounded-lg border border-white/10 bg-black/50 px-3 py-2 font-mono text-xs text-zinc-200",placeholder:"192.168.1.10:5555",value:L,onChange:e=>M(e.target.value),onKeyDown:e=>"Enter"===e.key&&void eM()}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eM(),className:"rounded-lg bg-sky-500/25 px-3 py-2 text-xs font-semibold text-sky-100 ring-1 ring-sky-400/40",children:el?"…":"Connect"}),(0,n.jsx)("button",{type:"button",disabled:el||x||!L.trim(),onClick:()=>eL(L),className:"rounded-lg border border-white/10 px-3 py-2 text-xs text-zinc-400 hover:text-zinc-200",children:s("存书签","Pin")})]}),er.length?(0,n.jsx)("div",{className:"flex flex-wrap gap-1.5 pt-1",children:er.map(e=>(0,n.jsx)("button",{type:"button",className:"rounded-md bg-white/[0.06] px-2 py-0.5 font-mono text-[10px] text-zinc-400 hover:bg-white/10",onClick:()=>M(e),children:e},e))}):null,(0,n.jsxs)("div",{className:"flex flex-wrap gap-2 pt-2",children:[(0,n.jsx)("button",{type:"button",disabled:el||x||!r,onClick:()=>void eT(r),className:"rounded-lg border border-rose-500/30 bg-rose-500/10 px-2.5 py-1.5 text-[10px] text-rose-200",children:s("断开当前 TCP","Disconnect current")}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eT(""),className:"rounded-lg border border-white/10 px-2.5 py-1.5 text-[10px] text-zinc-500 hover:text-zinc-300",children:s("断开全部 TCP","Disconnect all TCP")}),(0,n.jsxs)("button",{type:"button",disabled:el||x,onClick:()=>{b(),eP(),p(s("已刷新设备列表","Devices refreshed"))},className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-2.5 py-1.5 text-[10px] text-zinc-400",children:[(0,n.jsx)(y.Z,{className:"h-3 w-3"}),"ADB list"]})]})]}),(0,n.jsxs)("div",{className:"space-y-2 rounded-xl border border-white/[0.06] bg-black/35 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:"adb pair(无线调试)"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("input",{className:"w-32 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs",placeholder:"IP",value:T,onChange:e=>U(e.target.value)}),(0,n.jsx)("input",{className:"w-20 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs",placeholder:"port",value:E,onChange:e=>I(e.target.value)}),(0,n.jsx)("input",{className:"min-w-[6rem] flex-1 rounded-lg border border-white/10 bg-black/50 px-2 py-1.5 font-mono text-xs tracking-widest",placeholder:"123456",value:A,onChange:e=>en(e.target.value)}),(0,n.jsx)("button",{type:"button",disabled:el||x,onClick:()=>void eU(),className:"rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs text-violet-100 ring-1 ring-violet-400/35",children:"Pair"})]}),(0,n.jsx)("p",{className:"text-[10px] leading-relaxed text-zinc-600",children:s("手机上「无线调试」会显示配对端口与 6 位码;配对成功后用上方 connect 连接 adb 显示的 IP:5555。","Use the pairing port + 6-digit code from Wireless debugging; then connect to the listed adb IP:5555.")})]})]})]}),(0,n.jsxs)("div",{className:"relative overflow-hidden rounded-3xl border border-emerald-500/25 bg-gradient-to-br from-emerald-500/[0.12] via-zinc-950/90 to-violet-500/[0.08] p-1 shadow-2xl shadow-emerald-900/20 ring-1 ring-white/[0.06]",children:[(0,n.jsx)("div",{className:"pointer-events-none absolute -right-20 -top-20 h-56 w-56 rounded-full bg-emerald-500/10 blur-3xl","aria-hidden":!0}),(0,n.jsxs)("div",{className:"relative rounded-[1.35rem] border border-white/[0.06] bg-zinc-950/80 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between",children:[(0,n.jsxs)("div",{className:"flex items-start gap-3",children:[(0,n.jsx)("div",{className:"flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-400/30 to-violet-500/25 ring-1 ring-white/10",children:(0,n.jsx)(m.Z,{className:"h-6 w-6 text-emerald-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-base font-semibold tracking-tight text-white",children:s("无人直播系统 \xb7 ADB 云控台","Unmanned Live System \xb7 ADB farm console")}),(0,n.jsxs)("p",{className:"mt-1 max-w-xl text-[11px] leading-relaxed text-zinc-500",children:[s("统一适配 USB / Redroid / 无线 adb:已授权设备默认走官方 scrcpy-server 的 WebSocket H.264 真流(与桌面 ","USB / Redroid / wireless adb: authorized devices default to official scrcpy-server WebSocket H.264 (same stack as "),(0,n.jsx)("a",{href:et,target:"_blank",rel:"noreferrer",className:"text-emerald-300/90 underline-offset-2 hover:underline",children:"escrcpy"}),s(" / "," / "),(0,n.jsx)("a",{href:ee,target:"_blank",rel:"noreferrer",className:"text-emerald-300/90 underline-offset-2 hover:underline",children:"scrcpy"}),s(");无 jar 或关闭真流时用截图。","); PNG screenshot when jar missing or stream off.")]})]})]}),(0,n.jsxs)("div",{className:"flex shrink-0 flex-col gap-1.5 self-start sm:flex-row",children:[(0,n.jsxs)("a",{href:et,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2 text-[11px] font-medium text-zinc-300 hover:bg-white/[0.07]",children:["escrcpy",(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 opacity-70"})]}),(0,n.jsxs)("a",{href:ee,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/[0.04] px-3 py-2 text-[11px] font-medium text-zinc-300 hover:bg-white/[0.07]",children:["scrcpy",(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 opacity-70"})]})]})]}),a.length>0?(0,n.jsxs)("div",{className:"mt-5",children:[(0,n.jsxs)("p",{className:"mb-2 flex items-center gap-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:[(0,n.jsx)(O.Z,{className:"h-3.5 w-3.5"}),s("设备矩阵","Device matrix")]}),(0,n.jsx)("div",{className:"flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",children:a.map(e=>{let t="device"===e.state,l=r===e.serial,i=Q(e.transport),a=i.Icon;return(0,n.jsxs)("button",{type:"button",onClick:()=>o(e.serial),className:"flex min-w-[9rem] flex-col rounded-xl border px-3 py-2 text-left transition ".concat(l?"border-emerald-500/50 bg-emerald-500/15 ring-1 ring-emerald-400/30":"border-white/10 bg-black/30 hover:border-white/20"),children:[(0,n.jsxs)("span",{className:"flex items-center gap-1.5 text-xs font-medium text-zinc-200",children:[(0,n.jsx)(d.Z,{className:"h-3.5 w-3.5 text-emerald-400/80"}),(0,n.jsx)("span",{className:"truncate",children:e.model||e.serial.slice(0,10)})]}),(0,n.jsx)("span",{className:"mt-0.5 truncate font-mono text-[9px] text-zinc-600",children:e.serial}),(0,n.jsxs)("div",{className:"mt-1 flex flex-wrap gap-1",children:[(0,n.jsxs)("span",{className:"inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-[9px] font-semibold ring-1 ".concat(i.cls),children:[(0,n.jsx)(a,{className:"h-2.5 w-2.5"}),s(i.zh,i.en)]}),(0,n.jsx)("span",{className:"rounded-md px-1.5 py-0.5 text-[9px] font-semibold ".concat(t?"bg-emerald-500/20 text-emerald-200":"bg-zinc-600/30 text-zinc-500"),children:e.state})]})]},e.serial)})})]}):null,(0,n.jsxs)("div",{className:"mt-6 grid gap-6 xl:grid-cols-12",children:[(0,n.jsxs)("div",{className:"xl:col-span-8",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 border-b border-white/[0.06] pb-3",children:[(0,n.jsx)("span",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:s("刷新间隔","Interval")}),$.map(e=>(0,n.jsx)("button",{type:"button",onClick:()=>w(e),className:"rounded-lg px-2.5 py-1 text-[10px] font-semibold transition ".concat(v===e?"bg-violet-500/25 text-violet-100 ring-1 ring-violet-400/35":"bg-black/30 text-zinc-500 hover:text-zinc-300"),children:0===e?s("手动","Manual"):"".concat(e,"ms")},e)),(0,n.jsxs)("span",{className:"ml-1 inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[9px] font-medium ring-1 ".concat(eZ.cls),children:[(0,n.jsx)(eO,{className:"h-3 w-3"}),"tcp"===e_?s("建议 ≥450ms","Suggest ≥450ms"):s("可试 280ms","Try 280ms")]}),(0,n.jsxs)("button",{type:"button",disabled:x,onClick:()=>void eP(),className:"ml-auto inline-flex items-center gap-1 rounded-lg border border-white/10 bg-white/[0.05] px-2.5 py-1 text-[10px] text-zinc-300 hover:bg-white/[0.08]",children:[(0,n.jsx)(y.Z,{className:"h-3 w-3 ".concat(N?"animate-spin":"")}),s("下一帧","Next frame")]}),(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>void h("wake"),className:"rounded-lg bg-amber-500/20 px-2.5 py-1 text-[10px] font-semibold text-amber-100 ring-1 ring-amber-500/30",children:"Wake"}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-1.5 text-[10px] text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/50",checked:R,onChange:e=>P(e.target.checked)}),(0,n.jsx)(H.Z,{className:"h-3 w-3"}),s("点按穿透","Tap-through")]}),(0,n.jsxs)("button",{type:"button",disabled:x||!r||!(null==eb?void 0:eb.server_jar),title:eb&&!eb.server_jar&&eb.hint_zh?eb.hint_zh:s("官方 scrcpy-server + WebSocket H.264","Official scrcpy-server + WebSocket H.264"),onClick:()=>{ec(e=>{let t=!e;return t?(ex(null),p(s("已开启实时画面(H.264 WebSocket)","Live H.264 WebSocket stream on"))):(r&&ex(r),p(s("已关闭实时画面(将使用截图)","Live stream off — screenshot mode"))),t})},className:"inline-flex items-center gap-1 rounded-lg border px-2.5 py-1 text-[10px] font-semibold transition ".concat(eo?"border-cyan-400/50 bg-cyan-500/20 text-cyan-100 ring-1 ring-cyan-400/35":"border-white/10 bg-black/30 text-zinc-400 hover:text-zinc-200"),children:[(0,n.jsx)(B.Z,{className:"h-3 w-3"}),eo?s("关闭真流","Stop WS"):s("实时画面","Live H.264")]})]}),eb&&!eb.server_jar?(0,n.jsxs)("p",{className:"mt-2 text-[10px] text-amber-200/80",children:[s("未检测到 scrcpy-server:启动时会自动拉取;内网可在 runtime.env 配置 SCRCPY_SERVER_DOWNLOAD_URL(支持 {tag})。","scrcpy-server missing: auto-fetched on startup; set SCRCPY_SERVER_DOWNLOAD_URL ({tag}) for mirrors."),eb.server_version?" (".concat(s("期望版本","version")," ").concat(eb.server_version,")"):null]}):null,(0,n.jsx)("p",{className:"mt-2 text-[10px] text-zinc-600",children:s("快捷键(镜像区聚焦时):H Home \xb7 B Back \xb7 R 多任务 \xb7 W 唤醒 \xb7 M 菜单","Hotkeys (mirror focused): H Home \xb7 B Back \xb7 R Recents \xb7 W Wake \xb7 M Menu")}),(0,n.jsxs)("div",{className:"relative mx-auto mt-4 max-h-[min(78vh,720px)] min-h-[280px]",children:[r?(0,n.jsxs)("div",{ref:ez,tabIndex:0,onKeyDown:e=>{if(!r||x)return;let t=e.target.tagName;if("INPUT"===t||"TEXTAREA"===t||"SELECT"===t)return;let s={h:["key",{keycode:"3"}],b:["key",{keycode:"4"}],r:["key",{keycode:"187"}],w:["wake",void 0],m:["key",{keycode:"82"}]}[e.key.toLowerCase()];s&&(e.preventDefault(),(async()=>{await h(s[0],s[1]),eo||eP()})())},className:"relative flex justify-center outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50",children:[(0,n.jsxs)("div",{className:"relative rounded-[2rem] border-[10px] border-zinc-800 bg-black shadow-inner shadow-black/80 ring-1 ring-white/10",style:{maxHeight:"min(72vh, 680px)"},children:[(0,n.jsx)("div",{className:"absolute left-1/2 top-0 z-10 h-5 w-24 -translate-x-1/2 rounded-b-xl bg-black/80"}),eo?(0,n.jsx)("canvas",{ref:eN,className:"max-h-[min(72vh,680px)] w-auto max-w-full rounded-[1.35rem] object-contain ".concat(R?"cursor-crosshair":"cursor-default"),draggable:!1,onClick:eA,onContextMenu:eD}):(0,n.jsx)("img",{ref:ew,src:eR||void 0,alt:"device",className:"max-h-[min(72vh,680px)] w-auto max-w-full rounded-[1.35rem] object-contain ".concat(R?"cursor-crosshair":"cursor-default"),draggable:!1,onLoadStart:()=>{j(!0)},onLoad:()=>{j(!1),eS.current=0,z(null)},onError:()=>{j(!1),eS.current+=1,eS.current<=3?(z(null),window.setTimeout(()=>eP(),380+200*eS.current)):z(s("截图连续失败:检查 USB 授权 / 无线是否断开 / Redroid 端口","Screenshot failed repeatedly"))},onClick:eE,onContextMenu:eI})]}),eu?(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-cyan-500/90 px-2 py-0.5 text-[9px] font-bold text-cyan-950 shadow-lg",children:[(0,n.jsx)(B.Z,{className:"h-3 w-3"}),"SCRCPY\xb7WS"]}):v>0&&!eo?(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-emerald-500/90 px-2 py-0.5 text-[9px] font-bold text-emerald-950 shadow-lg",children:[(0,n.jsx)(K.Z,{className:"h-3 w-3"}),"LIVE"]}):eo?null:(0,n.jsxs)("div",{className:"pointer-events-none absolute bottom-3 right-3 flex items-center gap-1 rounded-full bg-zinc-700/90 px-2 py-0.5 text-[9px] font-bold text-zinc-200",children:[(0,n.jsx)(F.Z,{className:"h-3 w-3"}),s("单帧","Still")]})]}):(0,n.jsx)("p",{className:"py-16 text-center text-sm text-zinc-600",children:s("请选择或连接设备","Pick or connect a device")}),eh?(0,n.jsx)("p",{className:"mt-3 text-center text-xs text-rose-300",children:eh}):null,k&&!eo?(0,n.jsx)("p",{className:"mt-3 text-center text-xs text-rose-300",children:k}):null,(0,n.jsx)("p",{className:"mt-3 text-center text-[10px] text-zinc-600",children:s("左键单击 / 双击 \xb7 右键长按 \xb7 坐标对齐 PNG 像素","Left tap/double \xb7 right long-press \xb7 PNG pixel coords")})]})]}),(0,n.jsxs)("div",{className:"space-y-4 xl:col-span-4",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-black/40 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:s("遥测","Telemetry")}),u?(0,n.jsxs)("dl",{className:"mt-3 space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"device"}),(0,n.jsxs)("dd",{className:"text-right text-zinc-200",children:[u.brand," ",u.model]})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"Android"}),(0,n.jsxs)("dd",{className:"text-zinc-300",children:[u.android_version," (API ",u.sdk,")"]})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"wm size"}),(0,n.jsx)("dd",{className:"text-cyan-300/90",children:S?"".concat(S.width,"\xd7").concat(S.height):u.resolution||"—"})]}),(0,n.jsx)("div",{className:"whitespace-pre-wrap text-[10px] leading-snug text-zinc-500",children:u.battery})]}):(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-600",children:s("在上方点「刷新设备信息」","Use Refresh device info above")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/20 bg-gradient-to-b from-violet-500/[0.08] to-black/40 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-violet-300/80",children:s("导航坞","Nav dock")}),(0,n.jsx)("div",{className:"mt-3 grid grid-cols-3 gap-2",children:[["3","Home"],["4","Back"],["187","Recents"]].map(e=>{let[t,s]=e;return(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:t}),eP()},className:"rounded-xl border border-white/10 bg-white/[0.06] py-3 text-xs font-semibold text-zinc-200 hover:bg-white/[0.1]",children:s},t)})}),(0,n.jsxs)("div",{className:"mt-2 grid grid-cols-2 gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:"24"}),eP()},className:"rounded-xl border border-white/10 bg-black/30 py-2 text-[11px] text-zinc-300",children:"Vol+"}),(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("key",{keycode:"25"}),eP()},className:"rounded-xl border border-white/10 bg-black/30 py-2 text-[11px] text-zinc-300",children:"Vol−"})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-cyan-500/[0.05] p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-cyan-300/80",children:s("手势","Gestures")}),(0,n.jsx)("div",{className:"mt-2 grid grid-cols-2 gap-2",children:["up","down","left","right"].map(e=>(0,n.jsx)("button",{type:"button",disabled:x||!r,onClick:()=>{h("swipe",{direction:e}),eP()},className:"rounded-lg border border-white/10 bg-black/30 py-2 font-mono text-[10px] uppercase text-zinc-300",children:e},e))})]})]})]})]})]})]})}let el=[{label:"getprop",cmd:"getprop ro.build.version.release"},{label:"top pkg",cmd:"dumpsys activity activities | grep -E 'mResumedActivity' | tail -n 1"},{label:"usb",cmd:"dumpsys usb | head -n 60"},{label:"surface",cmd:"dumpsys SurfaceFlinger | head -n 40"}];function ei(e,t,s){return e?(0,n.jsx)("span",{className:"rounded bg-emerald-500/15 px-2 py-0.5 text-[10px] text-emerald-200",children:t}):(0,n.jsx)("span",{className:"rounded bg-zinc-500/15 px-2 py-0.5 text-[10px] text-zinc-400",children:s})}function er(e){var t,s,l,i,r,a,o,c,d,x,u,m,h,p,b;let{t:g,notify:f,fetchJson:y,apiBase:w,busy:N,androidSerial:j,setAndroidSerial:k,devices:z,adbAvailable:S,apkPath:C,setApkPath:R,onAndroidAction:P,onCopy:I,onRefreshDevices:A}=e,[D,Z]=(0,_.useState)(null),[O,H]=(0,_.useState)(null),[B,K]=(0,_.useState)(null),[F,W]=(0,_.useState)(null),[Y,J]=(0,_.useState)(""),[G,V]=(0,_.useState)([]),[q,X]=(0,_.useState)("getprop ro.product.model"),[$,Q]=(0,_.useState)(""),[ee,et]=(0,_.useState)("300"),[es,er]=(0,_.useState)(""),[ea,eo]=(0,_.useState)(null),[ec,ed]=(0,_.useState)([]),[ex,eu]=(0,_.useState)(null),[em,eh]=(0,_.useState)("https://"),[ep,eb]=(0,_.useState)(""),[eg,ef]=(0,_.useState)(!1),[ev,ey]=(0,_.useState)(null),ew=!!(N||ev),eN=z.find(e=>e.serial===j)||null,ej=(0,_.useCallback)(async()=>{if(!j||!S){Z(null),H(null);return}ey("overview"),H(null);try{let e=await y("/android/overview?serial=".concat(encodeURIComponent(j)));Z(e)}catch(e){Z(null),H(e.message)}finally{ey(null)}},[S,j,y]),ek=(0,_.useCallback)(async()=>{W(null);try{let e=await y("/redroid/summary");K(e)}catch(e){K(null),W(e.message)}},[y]);(0,_.useEffect)(()=>{ej()},[ej]),(0,_.useEffect)(()=>{ek()},[ek]);let ez=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}ey("packages");try{let e=encodeURIComponent(Y.trim()),t=await y("/android/packages?serial=".concat(encodeURIComponent(j),"&query=").concat(e,"&limit=80"));V(t.packages||[])}catch(e){f(e.message),V([])}finally{ey(null)}},eS=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}let e=q.trim();if(e){ey("shell");try{var t,s;let n=await y("/android/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:"shell",serial:j,payload:{command:e}})});Q(null!==(s=null!==(t=n.output)&&void 0!==t?t:n.message)&&void 0!==s?s:"")}catch(e){Q(e.message)}finally{ey(null)}}},e_=(0,_.useCallback)(async e=>{if(!j)return;let t=Math.min(3e3,Math.max(1,parseInt(ee,10)||200)),s=!!(null==e?void 0:e.silent);s||ey("logcat"),eo(null);try{var n;let e=await y("/android/logcat?serial=".concat(encodeURIComponent(j),"&lines=").concat(t));er(null!==(n=e.text)&&void 0!==n?n:"")}catch(e){er(""),eo(e.message)}finally{s||ey(null)}},[j,y,ee]);(0,_.useEffect)(()=>{if(!eg||!j||!S)return;let e=window.setInterval(()=>void e_({silent:!0}),3e3);return e_({silent:!0}),()=>clearInterval(e)},[S,j,eg,e_]);let eC=async()=>{if(!j){f(g("请选择设备","Pick a device"));return}ey("ui"),eu(null);try{let e=await y("/android/ui?serial=".concat(encodeURIComponent(j),"&limit=100"));ed(e.nodes||[])}catch(e){ed([]),eu(e.message)}finally{ey(null)}},eR=async(e,t)=>{ey("redroid:".concat(e));try{let s=await y("/redroid/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,name:t||""})});s.summary&&K(s.summary),f(s.message||"OK"),await ek(),A()}catch(e){f(e.message)}finally{ey(null)}},eP=async e=>{await P("key",{keycode:e})},eL=async()=>{let e=ep.trim();if(!e){f(g("请输入文字","Enter text"));return}await P("text",{text:e})},eM=async()=>{let e=em.trim();if(!e||"https://"===e){f(g("请输入 URL","Enter URL"));return}await P("open_url",{url:e})};return(0,n.jsxs)("div",{className:"mx-auto max-w-6xl space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("安卓设备工作台","Android workstation")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:g("USB 真机与 Redroid 共用一套控制面,优先保证 USB 真机稳定。","USB phone and Redroid share one control surface.")})]}),S?(0,n.jsxs)("div",{className:"flex gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew||!j,onClick:()=>void ej(),className:"rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300",children:g("刷新设备","Refresh device")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>{ek(),A()},className:"rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300",children:g("刷新 Redroid","Refresh Redroid")})]}):(0,n.jsx)("span",{className:"text-xs text-amber-200/90",children:g("宿主机未检测到 adb","adb not found on host")})]}),(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:z.map(e=>(0,n.jsxs)("button",{type:"button",onClick:()=>k(e.serial),className:"rounded-xl border px-4 py-2 text-left text-xs ".concat(j===e.serial?"border-violet-500/50 bg-violet-500/10 text-white":"border-white/10 bg-black/30 text-zinc-400"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2",children:[(0,n.jsx)("span",{className:"font-medium text-zinc-200",children:e.model||e.serial}),e.transport?(0,n.jsx)("span",{className:"rounded bg-white/10 px-1.5 py-0.5 font-mono text-[9px] uppercase text-zinc-500",children:e.transport}):null,e.kind?(0,n.jsx)("span",{className:"rounded bg-cyan-500/10 px-1.5 py-0.5 font-mono text-[9px] uppercase text-cyan-200",children:e.kind}):null]}),(0,n.jsx)("div",{className:"text-[10px] text-zinc-600",children:e.serial}),"device"!==(e.state||"").toLowerCase()?(0,n.jsx)("div",{className:"mt-1 text-[10px] font-medium text-amber-200/90",children:"unauthorized"===e.state?g("未授权,请在手机上允许 USB 调试。","Unauthorized, allow USB debugging on device."):"".concat(g("状态","State"),": ").concat(e.state)}):null]},e.serial))}),S&&0===z.length?(0,n.jsx)("p",{className:"mt-4 rounded-lg border border-amber-500/20 bg-amber-500/5 p-3 text-xs leading-relaxed text-amber-200/90",children:g("当前没有已授权设备。USB 真机请检查调试授权;Redroid 请先启动实例,再 adb connect 127.0.0.1:5555。","No authorized devices. For USB phones, confirm USB debugging. For Redroid, start an instance then adb connect 127.0.0.1:5555.")}):null]}),(0,n.jsxs)("div",{className:"grid gap-4 xl:grid-cols-[1.1fr_0.9fr]",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/15 bg-zinc-950/50 p-5",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-violet-300/90",children:g("真机诊断","Phone diagnostics")}),O?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:O}):null,D?(0,n.jsxs)("div",{className:"mt-3 space-y-3",children:[(0,n.jsxs)("dl",{className:"space-y-1.5 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"model"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[D.brand," ",D.model]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"android"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[D.android_version," (sdk ",D.sdk,")"]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"kind"})," ",(0,n.jsxs)("dd",{className:"inline text-zinc-300",children:[D.kind||(null==eN?void 0:eN.kind)||"unknown"," / ",D.transport||(null==eN?void 0:eN.transport)||"unknown"]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"display"})," ",(0,n.jsx)("dd",{className:"inline text-zinc-300",children:D.resolution})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"inline text-zinc-600",children:"rotation"})," ",(0,n.jsx)("dd",{className:"inline text-zinc-300",children:D.rotation})]}),(0,n.jsx)("div",{className:"whitespace-pre-wrap text-[10px] leading-snug text-zinc-500",children:D.battery}),(0,n.jsx)("div",{className:"line-clamp-3 text-[10px] text-zinc-500",children:D.focus})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[ei(null===(t=D.diagnostics)||void 0===t?void 0:t.ready,g("已启动","Boot ready"),g("未完成启动","Boot pending")),ei(null===(s=D.diagnostics)||void 0===s?void 0:s.google_play,"Google Play","No Play"),ei(null===(l=D.diagnostics)||void 0===l?void 0:l.google_services,"GMS","No GMS"),ei(null===(i=D.diagnostics)||void 0===i?void 0:i.magisk,"Magisk","No Magisk"),ei(null===(r=D.diagnostics)||void 0===r?void 0:r.root,"Root","No root")]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/5 bg-black/20 p-3 text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:["boot_completed: ",(null===(a=D.diagnostics)||void 0===a?void 0:a.boot_completed)||"-"]}),(0,n.jsxs)("div",{children:["provisioned: ",(null===(o=D.diagnostics)||void 0===o?void 0:o.device_provisioned)||"-"]}),(0,n.jsxs)("div",{children:["setup_complete: ",(null===(c=D.diagnostics)||void 0===c?void 0:c.user_setup_complete)||"-"]}),(0,n.jsxs)("div",{children:["abi: ",(null===(d=D.diagnostics)||void 0===d?void 0:d.abi)||"-"]}),(0,n.jsxs)("div",{className:"truncate",children:["hardware: ",(null===(x=D.diagnostics)||void 0===x?void 0:x.hardware)||"-"]})]})]}):O?null:(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:g("加载中","Loading")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-zinc-950/50 p-5",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-3",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-cyan-300/90",children:g("Redroid 板块","Redroid panel")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("start"),className:"rounded-lg bg-emerald-500/20 px-3 py-1.5 text-xs text-emerald-100 ring-1 ring-emerald-500/30",children:g("启动默认实例","Start default")})]}),F?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:F}):null,B?(0,n.jsxs)("div",{className:"mt-3 space-y-3 text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsxs)("div",{children:["arch: ",B.arch||"-"]}),(0,n.jsxs)("div",{className:"truncate",children:["image: ",B.image||"-"]}),(0,n.jsxs)("div",{children:["profile: ",B.device_profile||"-"]}),(0,n.jsxs)("div",{children:["gpu: ",B.gpu_mode||"-"]}),(0,n.jsxs)("div",{children:["adb: 127.0.0.1:",B.default_port||5555]})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[ei(null===(u=B.requirements)||void 0===u?void 0:u.docker_available,"Docker","No Docker"),ei(null===(m=B.requirements)||void 0===m?void 0:m.adb_available,"ADB","No ADB"),ei(null===(h=B.requirements)||void 0===h?void 0:h.kvm,"KVM","No KVM"),ei(null===(p=B.requirements)||void 0===p?void 0:p.binderfs,"binderfs","No binderfs")]}),(0,n.jsxs)("div",{className:"space-y-2",children:[(B.instances||[]).map(e=>{var t,s,l,i;return(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-3",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"font-medium text-zinc-100",children:e.name}),(0,n.jsxs)("div",{className:"font-mono text-[10px] text-zinc-500",children:[e.port?"127.0.0.1:".concat(e.port):"-"," / ",e.status_text||"-"]})]}),(0,n.jsxs)("div",{className:"flex gap-1",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("start",e.name),className:"rounded bg-emerald-500/15 px-2 py-1 text-[10px] text-emerald-200",children:"Start"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("restart",e.name),className:"rounded bg-amber-500/15 px-2 py-1 text-[10px] text-amber-200",children:"Restart"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("adb_connect",e.name),className:"rounded bg-cyan-500/15 px-2 py-1 text-[10px] text-cyan-200",children:"ADB"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eR("stop",e.name),className:"rounded bg-rose-500/15 px-2 py-1 text-[10px] text-rose-200",children:"Stop"})]})]}),(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap gap-2",children:[ei(null===(t=e.diagnostics)||void 0===t?void 0:t.ready,g("系统就绪","System ready"),g("启动中","Booting")),ei(null===(s=e.diagnostics)||void 0===s?void 0:s.google_play,"Play","No Play"),ei(null===(l=e.diagnostics)||void 0===l?void 0:l.magisk,"Magisk","No Magisk"),ei(null===(i=e.diagnostics)||void 0===i?void 0:i.root,"Root","No root")]}),e.data_dir?(0,n.jsx)("div",{className:"mt-2 truncate font-mono text-[10px] text-zinc-500",children:e.data_dir}):null]},e.name)}),(null===(b=B.instances)||void 0===b?void 0:b.length)?null:(0,n.jsx)("p",{className:"rounded-xl border border-white/5 bg-black/20 p-3 text-xs text-zinc-500",children:g("当前没有 Redroid 实例。默认动作会创建 redroid13-1。","No Redroid instances yet. Default actions create redroid13-1.")})]})]}):(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:g("读取 Redroid 状态中","Loading Redroid status")})]})]}),(0,n.jsx)(en,{t:g,apiBase:w,fetchJson:y,serial:j,devices:z,setSerial:k,adbAvailable:S,lock:ew,overview:D,onAndroidAction:P,notify:f,onRefreshDevices:A}),S&&j?(0,n.jsxs)("div",{className:"grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-emerald-300/90",children:g("快捷控制","Quick controls")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("3"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Home"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("4"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Back"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("187"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Recents"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("wake"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Wake"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("launch_package",{package:"com.zhiliaoapp.musically"}),className:"rounded-lg bg-gradient-to-r from-pink-500/25 to-violet-500/25 px-3 py-1.5 text-xs ring-1 ring-white/10",children:"TikTok"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("24"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Vol+"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("25"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Vol-"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eP("66"),className:"rounded-lg border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs",children:"Enter"})]}),(0,n.jsx)("p",{className:"mt-3 text-[10px] text-zinc-600",children:g("滑动手势","Swipe gestures")}),(0,n.jsx)("div",{className:"mt-1 flex flex-wrap gap-2",children:["up","down","left","right"].map(e=>(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("swipe",{direction:e}),className:"rounded-lg border border-white/10 bg-black/30 px-2.5 py-1 font-mono text-[10px] uppercase text-zinc-300",children:e},e))}),(0,n.jsxs)("div",{className:"mt-4 rounded-lg border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:g("ADB 文本输入","ADB text input")}),(0,n.jsxs)("div",{className:"mt-2 flex flex-col gap-2 sm:flex-row",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-xs",value:ep,onChange:e=>eb(e.target.value),placeholder:"hello / 123",onKeyDown:e=>{"Enter"===e.key&&eL()}}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eL(),className:"rounded-lg bg-emerald-500/20 px-3 py-2 text-xs text-emerald-100 ring-1 ring-emerald-500/30",children:g("发送","Send")})]})]}),(0,n.jsxs)("div",{className:"mt-4 flex flex-col gap-2 sm:flex-row sm:items-center",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-xs",value:em,onChange:e=>eh(e.target.value),placeholder:"https://"}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eM(),className:"rounded-lg bg-violet-500/20 px-3 py-2 text-xs text-violet-100 ring-1 ring-violet-500/30",children:g("打开链接","Open URL")})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsxs)("h4",{className:"flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-cyan-300/90",children:[(0,n.jsx)(L.Z,{className:"h-4 w-4"}),g("USB 真机优化建议","USB optimization")]}),(0,n.jsxs)("ul",{className:"mt-3 space-y-2 text-xs leading-relaxed text-zinc-400",children:[(0,n.jsx)("li",{children:g("优先使用主板直连 USB,不要经过不稳的 HUB。","Prefer direct motherboard USB, not unstable hubs.")}),(0,n.jsx)("li",{children:g("开发板端固定 adb server,避免频繁 kill-server。","Keep adb server stable and avoid frequent kill-server.")}),(0,n.jsx)("li",{children:g("镜像页默认支持 scrcpy H.264 真流,截图模式作为回退。","Use scrcpy H.264 live stream first, screenshot mode as fallback.")}),(0,n.jsx)("li",{children:g("如果 TikTok / YouTube App 直播,要先确认屏幕常亮、分辨率稳定、USB 供电足够。","For TikTok / YouTube app live, keep screen on, resolution stable, and USB power sufficient.")})]})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(M.Z,{className:"h-4 w-4 text-emerald-400"}),g("包名搜索与启动","Packages")]}),(0,n.jsxs)("div",{className:"mt-3 flex flex-col gap-2 sm:flex-row",children:[(0,n.jsx)("input",{className:"min-w-0 flex-1 rounded-xl border border-white/10 bg-black/40 px-4 py-2 text-sm",value:Y,onChange:e=>J(e.target.value),placeholder:"tiktok / musically / youtube",onKeyDown:e=>{"Enter"===e.key&&ez()}}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void ez(),className:"rounded-xl bg-emerald-500/20 px-4 py-2 text-sm text-emerald-100 ring-1 ring-emerald-500/30",children:g("搜索","Search")})]}),(0,n.jsxs)("ul",{className:"mt-4 max-h-48 space-y-1 overflow-y-auto rounded-lg border border-white/5 bg-black/30 p-2 font-mono text-[11px]",children:[G.map(e=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 py-1 text-zinc-400",children:[(0,n.jsx)("span",{className:"min-w-0 truncate text-zinc-300",children:e.package}),(0,n.jsxs)("span",{className:"flex shrink-0 gap-1",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("launch_package",{package:e.package}),className:"rounded bg-white/10 px-2 py-0.5 text-[10px] text-zinc-200",children:g("启动","Launch")}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("force_stop",{package:e.package}),className:"rounded bg-rose-500/15 px-2 py-0.5 text-[10px] text-rose-200",children:g("停止","Stop")})]})]},e.package)),G.length?null:(0,n.jsx)("li",{className:"py-2 text-center text-zinc-600",children:g("暂无结果","No results")})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"grid gap-6 xl:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(T.Z,{className:"h-4 w-4 text-amber-400"}),"ADB shell"]}),(0,n.jsx)("div",{className:"mt-2 flex flex-wrap gap-1.5",children:el.map(e=>(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>X(e.cmd),className:"rounded-md border border-white/10 bg-white/[0.04] px-2 py-1 text-[10px] text-zinc-400 hover:text-zinc-200",children:e.label},e.label))}),(0,n.jsx)("textarea",{className:"mt-3 min-h-[100px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-3 font-mono text-xs text-zinc-200",value:q,onChange:e=>X(e.target.value),spellCheck:!1,onKeyDown:e=>{"Enter"===e.key&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),eS())}}),(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap items-center gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eS(),className:"rounded-xl bg-amber-500/20 px-4 py-2 text-sm text-amber-100 ring-1 ring-amber-500/30",children:"shell"===ev?(0,n.jsxs)("span",{className:"inline-flex items-center gap-2",children:[(0,n.jsx)(v.Z,{className:"h-3.5 w-3.5 animate-spin"}),"Run"]}):"Run"}),$?(0,n.jsxs)("button",{type:"button",disabled:!$,onClick:()=>I($),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-200",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),g("复制输出","Copy")]}):null]}),$?(0,n.jsx)("pre",{className:"mt-3 max-h-64 overflow-auto whitespace-pre-wrap rounded-lg border border-white/5 bg-black/40 p-3 font-mono text-[11px] text-zinc-400 xl:max-h-[min(24rem,50vh)]",children:$}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("Logcat","Logcat")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap items-center gap-2",children:[(0,n.jsx)("input",{className:"w-24 rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-xs",value:ee,onChange:e=>et(e.target.value)}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2 text-[11px] text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:eg,onChange:e=>ef(e.target.checked)}),g("每 3 秒自动刷新","Auto every 3s")]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void e_(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:g("拉取日志","Pull logs")}),es?(0,n.jsxs)("button",{type:"button",onClick:()=>I(es),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:text-zinc-200",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),g("复制","Copy")]}):null]}),ea?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:ea}):null,es?(0,n.jsx)("pre",{className:"mt-3 max-h-72 overflow-auto whitespace-pre-wrap rounded-lg border border-white/5 bg-black/50 p-3 font-mono text-[10px] leading-tight text-zinc-500 xl:max-h-[min(24rem,50vh)]",children:es}):null]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(E.Z,{className:"h-4 w-4 text-fuchsia-400"}),g("界面节点","UI nodes")]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void eC(),className:"mt-3 rounded-xl border border-fuchsia-500/30 bg-fuchsia-500/10 px-4 py-2 text-sm text-fuchsia-100",children:g("刷新节点","Refresh nodes")}),ex?(0,n.jsx)("p",{className:"mt-2 text-xs text-rose-300",children:ex}):null,(0,n.jsxs)("ul",{className:"mt-3 max-h-64 space-y-1 overflow-y-auto rounded-lg border border-white/5 bg-black/30 p-2 text-[11px]",children:[ec.map((e,t)=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 border-b border-white/[0.04] py-1.5 text-zinc-400 last:border-0",children:[(0,n.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,n.jsx)("span",{className:"text-zinc-200",children:e.label}),e.resource_id?(0,n.jsx)("span",{className:"ml-2 font-mono text-[10px] text-zinc-600",children:e.resource_id}):null,(0,n.jsxs)("div",{className:"font-mono text-[10px] text-zinc-600",children:["(",e.center_x,", ",e.center_y,")",e.clickable?" click":""]})]}),(0,n.jsx)("button",{type:"button",disabled:ew,onClick:()=>void P("tap",{x:e.center_x,y:e.center_y}),className:"shrink-0 rounded bg-white/10 px-2 py-1 text-[10px] text-zinc-200",children:"Tap"})]},"".concat(e.center_x,"-").concat(e.center_y,"-").concat(t))),ec.length||ex?null:(0,n.jsx)("li",{className:"py-4 text-center text-zinc-600",children:g("尚未加载","Not loaded")})]})]}):null,S&&j?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:g("安装 APK","Install APK")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:g("先 adb push 到 /sdcard/ 再执行安装。","Push the APK to /sdcard/ first.")}),(0,n.jsx)("input",{className:"mt-4 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm",value:C,onChange:e=>R(e.target.value)}),(0,n.jsx)("button",{type:"button",disabled:ew||!S||!j,onClick:()=>void P("install_apk",{path:C}),className:"mt-3 rounded-xl bg-emerald-500/20 px-4 py-2 text-sm font-medium text-emerald-100 ring-1 ring-emerald-500/30",children:"pm install"})]}):null]})}var ea=s(9897),eo=s(4554),ec=s(7689),ed=s(8930),ex=s(8401);function eu(e){let{t,title:s,subtitle:l,steps:i,onNavigate:r}=e;return(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/25 bg-gradient-to-br from-violet-500/10 via-zinc-950/40 to-fuchsia-500/5 p-6 shadow-lg shadow-violet-900/10",children:[(0,n.jsx)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:s}),(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-400",children:l}),(0,n.jsx)("div",{className:"mt-6 flex flex-col items-center gap-4 sm:flex-row sm:flex-wrap sm:justify-center",children:i.map((e,t)=>(0,n.jsxs)("div",{className:"flex items-center gap-4",children:[e.openUrl||e.target&&r?(0,n.jsxs)("button",{type:"button",onClick:()=>{if(e.openUrl){window.open((0,ex.p)(e.openUrl),"_blank","noopener,noreferrer");return}e.target&&r&&r(e.target)},className:"flex min-h-[4rem] min-w-[7rem] flex-col items-center justify-center rounded-2xl bg-gradient-to-br px-4 py-2 text-center text-xs font-bold text-white shadow-lg ring-2 ring-white/10 transition hover:ring-violet-400/50 hover:brightness-110 ".concat(e.gradient),children:[(0,n.jsx)("span",{children:e.label}),e.sub?(0,n.jsx)("span",{className:"mt-1 text-[10px] font-normal text-white/80",children:e.sub}):null]}):(0,n.jsxs)("div",{className:"flex min-h-[4rem] min-w-[7rem] flex-col items-center justify-center rounded-2xl bg-gradient-to-br px-4 py-2 text-center text-xs font-bold text-white shadow-lg ".concat(e.gradient),children:[(0,n.jsx)("span",{children:e.label}),e.sub?(0,n.jsx)("span",{className:"mt-1 text-[10px] font-normal text-white/80",children:e.sub}):null]}),t8?e.slice(-8):e;if(e.includes("live2/")){let t=e.split("/"),s=t[t.length-1]||"";return s.length>8?s.slice(-8):s}}}let t=e.trim();return!t||t.includes("\n")||t.toLowerCase().startsWith("rtmp")?"":t.length>8?t.slice(-8):t}function eh(e){let t=[];for(let s of e.split(/\r?\n/)){let e=s.trim();if(!e||e.startsWith("#")||!/douyin\.com|v\.douyin\.com/i.test(e))continue;let n=e.match(/(https?:\/\/live\.douyin\.com\/[a-zA-Z0-9_-]+)/i);if(n){t.push(n[1].replace(/^http:\/\//i,"https://"));continue}let l=e.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);if(l){t.push("https://live.douyin.com/".concat(l[1]));continue}let i=e.match(/(\d{8,})/);i&&t.push("https://live.douyin.com/".concat(i[1]))}return Array.from(new Set(t)).slice(0,16)}function ep(e){let t="",s=!0,n="",l=!1,i=e.split(/\r?\n/),r=!1;for(let e of i){let i=e.trim();if(/^\[youtube\]/i.test(i)){r=!0;continue}if(/^\[/.test(i)){r=!1;continue}if(!r||!i||i.startsWith("#")||i.startsWith(";"))continue;let a=i.indexOf("=");if(a<0)continue;let o=i.slice(0,a).trim().toLowerCase(),c=i.slice(a+1).trim().split(/[#;]/)[0].trim();"key"===o||"stream_key"===o?t=c:"rtmps"===o?s=!/^(0|false|否)/i.test(c):"bitrate"===o?n=c:"fast_audio"===o&&(l=/^(1|true|yes|是|开|on)/i.test(c))}return{key:t,rtmps:s,bitrate:n,fastAudio:l}}function eb(e){return"URL_config.".concat(e.replace(/[^a-zA-Z0-9_.-]/g,"_"),".ini")}function eg(e){let t=e.bitrate.trim()?"bitrate = ".concat(e.bitrate.trim()):"# bitrate = 4000";return"[youtube]\n# YouTube 推流 key(工作室 → 直播 → 串流密钥)\nkey = ".concat(e.key.trim(),"\n\n# 使用 RTMPS;填 否 则 RTMP\nrtmps = ").concat(e.rtmps?"是":"否","\n\n# 视频比特率 kbps(可选)\n").concat(t,"\n\n# 轻量音频链,减轻 CPU:1 / 是\nfast_audio = ").concat(e.fastAudio?"1":"0","\n")}function ef(e){if(e<60)return"".concat(e,"s");let t=Math.floor(e/60);return t<60?"".concat(t,"m ").concat(e%60,"s"):"".concat(Math.floor(t/60),"h ").concat(t%60,"m")}function ev(e){var t,s,l;let{t:i,currentProc:r,fetchJson:a,urlListText:o,keySuffixUi:c,showKeyRow:d=!0,pollMs:x=1e3,refreshToken:u=0}=e,[m,h]=(0,_.useState)(""),[p,b]=(0,_.useState)(""),[g,f]=(0,_.useState)(""),[v,w]=(0,_.useState)(""),[N,j]=(0,_.useState)(""),[k,z]=(0,_.useState)([]),[S,C]=(0,_.useState)(null),[R,P]=(0,_.useState)(!1),[L,M]=(0,_.useState)(null),[T,U]=(0,_.useState)(0),[E,I]=(0,_.useState)(!0),A=(0,_.useRef)(null),D=(0,_.useRef)(null),O=(0,_.useRef)(!1),H=(0,_.useRef)(!1),B=(0,_.useRef)(!1),W=(0,_.useRef)(r),Y=(0,_.useRef)(0),J=function(e){let t=[];for(let s of e.split(/\r?\n/)){let e=s.trim();if(!e||e.startsWith("#")||!/douyin\.com|v\.douyin\.com/i.test(e))continue;let n=e.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);if(n){t.push(n[1]);continue}let l=e.match(/(\d{8,})/);t.push(l?l[1]:e.slice(0,96))}return Array.from(new Set(t)).slice(0,12)}(o),G=/^(online|running)$/i.test(m),V=function(e){if(!e)return null;let t=null;for(let s of e.split(/\r?\n/)){let e=s.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i);e&&(t=e[1]);let n=s.match(/https?:\/\/[^/\s]*douyin\.com\/(\d{8,})/i);n&&(t=n[1])}return t}(p),q=null!==(s=eh(o)[0])&&void 0!==s?s:"",X=V||(null!==(l=null===(t=q.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i))||void 0===t?void 0:t[1])&&void 0!==l?l:null),$=X?"https://live.douyin.com/".concat(X):q||null,Q=(0,_.useCallback)(async()=>{let e=W.current.trim();if(!e)return;if(H.current){B.current=!0;return}H.current=!0;let t=++Y.current;try{let s=await a("/status?process=".concat(encodeURIComponent(e)));if(t!==Y.current||e!==W.current.trim())return;let n=s.process_status||"unknown";h(n),b(s.recent_log||""),f(s.recent_error||""),w(s.business_status||""),j(s.business_note||""),z(Array.isArray(s.ffmpeg_pids)?s.ffmpeg_pids:[]),C("number"==typeof s.log_age_seconds?s.log_age_seconds:null);let l=/^(online|running)$/i.test(n);l&&!O.current&&M(Date.now()),l||M(null),O.current=l}catch(s){if(t!==Y.current||e!==W.current.trim())return;h("error"),w("error"),j(""),z([]),C(null)}finally{H.current=!1,B.current&&(B.current=!1,window.setTimeout(()=>void Q(),0))}},[a]);(0,_.useEffect)(()=>{if(W.current=r,Y.current+=1,B.current=!1,!r){h(""),b(""),f(""),w(""),j(""),z([]),C(null),M(null),O.current=!1;return}h(""),b(""),f(""),w(""),j(""),z([]),C(null),M(null),O.current=!1,Q()},[r,Q]),(0,_.useEffect)(()=>{let e=()=>I("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{r&&Q()},[r,Q,u]),(0,_.useEffect)(()=>{if(R||!E||!r)return;let e=window.setInterval(()=>void Q(),x);return()=>clearInterval(e)},[R,E,r,Q,x]),(0,_.useEffect)(()=>{let e=()=>{"visible"===document.visibilityState&&Q()};return document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[Q]),(0,_.useEffect)(()=>{if(!L)return;let e=window.setInterval(()=>U(e=>e+1),1e3);return()=>clearInterval(e)},[L]),(0,_.useEffect)(()=>{if(R||!E)return;let e=A.current;e&&(e.scrollTop=e.scrollHeight)},[p,R,E]),(0,_.useEffect)(()=>{if(R||!E)return;let e=D.current;e&&(e.scrollTop=e.scrollHeight)},[g,R,E]);let ee=null==L?i("—(未检测到在线)","— (not online)"):ef(Math.max(0,Math.floor((Date.now()-L)/1e3))),et=R?i("已暂停刷新","Paused"):E?i("每秒刷新","~1s refresh"):i("后台暂停","Hidden"),es=R?"bg-amber-500/20 text-amber-100 ring-amber-500/35":E?"bg-emerald-500/15 text-emerald-200/90 ring-emerald-500/25":"bg-sky-500/15 text-sky-100 ring-sky-500/25";return(0,n.jsxs)("section",{className:"rounded-2xl border border-emerald-500/20 bg-gradient-to-b from-emerald-500/[0.07] to-zinc-950/80 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("span",{className:"sr-only","aria-hidden":!0,children:T}),(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:i("当前直播日志","Live process log")}),(0,n.jsx)("span",{className:"rounded-full bg-white/10 px-2 py-0.5 font-mono text-[10px] text-zinc-300 ring-1 ring-white/10",children:r||"—"}),(0,n.jsx)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-medium ring-1 ".concat(es),children:et})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsxs)("button",{type:"button",onClick:()=>void Q(),className:"inline-flex items-center gap-1 rounded-lg border border-white/10 bg-white/[0.05] px-3 py-1.5 text-xs text-zinc-300 hover:bg-white/[0.08]",children:[(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5"}),i("立即拉取","Refresh")]}),(0,n.jsxs)("button",{type:"button",onClick:()=>P(e=>!e),className:"inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-xs ring-1 ".concat(R?"bg-amber-500/20 text-amber-100 ring-amber-500/35":"border border-white/10 bg-black/30 text-zinc-300"),children:[R?(0,n.jsx)(K.Z,{className:"h-3.5 w-3.5"}):(0,n.jsx)(F.Z,{className:"h-3.5 w-3.5"}),R?i("继续自动刷新","Resume"):i("暂停自动刷新","Pause")]})]})]}),(0,n.jsxs)("div",{className:"mt-4 space-y-3 rounded-xl border border-white/[0.06] bg-black/25 p-4",children:[(0,n.jsxs)("div",{className:"grid gap-2 text-xs sm:grid-cols-2",children:[(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("进程状态","Status")}),(0,n.jsx)("span",{className:"font-medium text-zinc-200",children:function(e,t){let s=(t||"unknown").toLowerCase();return"online"===s||"running"===s?e("运行中(PM2/进程在线)","Running (process online)"):"stopped"===s||"stopping"===s?e("已停止","Stopped"):"not_found"===s||"errored"===s?e("未运行或未找到","Not running / not found"):"invalid"===s?e("无效进程名","Invalid process"):t||"—"}(i,m)})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("Business state","Business state")}),(0,n.jsx)("span",{className:"font-medium text-cyan-200/90",children:function(e,t,s){let n=(t||"").toLowerCase();return"streaming"===n?e("Pushing to YouTube","Pushing to YouTube"):"waiting_source"===n?e("Waiting for the upstream live source","Waiting for the upstream live source"):"source_live"===n?e("Source live detected, but no active FFmpeg relay worker","Source live detected, but no active FFmpeg relay worker"):"monitoring"===n?e("Process online and monitoring the source","Process online and monitoring the source"):"misconfigured"===n?e("YouTube key missing or invalid","YouTube key missing or invalid"):"stale"===n?e("Process online but the log heartbeat is stale","Process online but the log heartbeat is stale"):"error"===n?e("Recent process error detected","Recent process error detected"):(null==s?void 0:s.trim())?s.trim():e("No business-state hint yet","No business-state hint yet")}(i,v,N)})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("本次在线时长(估算)","Online duration (est.)")}),(0,n.jsx)("span",{className:"font-mono text-cyan-200/90",children:ee})]}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("FFmpeg workers","FFmpeg workers")}),(0,n.jsxs)("span",{className:"font-mono text-zinc-300",children:[k.length?k.join(", "):"none",null!=S?" \xb7 log ".concat(ef(Math.max(0,Math.floor(S)))):""]})]})]}),d?(0,n.jsxs)("div",{className:"flex flex-wrap gap-2 text-xs",children:[(0,n.jsx)("span",{className:"text-zinc-500",children:i("YouTube key 尾码(参考)","YT key suffix")}),(0,n.jsx)("span",{className:"font-mono text-zinc-300",children:c||"—"})]}):null,$?(0,n.jsxs)("div",{className:"rounded-lg border border-violet-500/20 bg-violet-500/[0.06] px-3 py-2.5",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsx)("span",{className:"text-[11px] font-medium text-zinc-400",children:V?i("当前直播源站(日志)","Current source (from log)"):G?i("配置中的源站地址","Configured source URL"):i("源站快捷入口","Source quick link")}),(0,n.jsxs)("a",{href:$,target:"_blank",rel:"noreferrer",className:"inline-flex items-center gap-1.5 rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs font-medium text-violet-100 ring-1 ring-violet-500/35 transition hover:bg-violet-500/30",children:[(0,n.jsx)(Z.Z,{className:"h-3.5 w-3.5 shrink-0 opacity-90"}),X||i("打开抖音直播页","Open Douyin live")]})]}),J.length>1&&V?(0,n.jsxs)("p",{className:"mt-2 text-[10px] text-zinc-600",children:[i("配置中还有","Also in config:")," ",J.filter(e=>e!==X).slice(0,3).map(e=>(0,n.jsx)("a",{href:"https://live.douyin.com/".concat(e),target:"_blank",rel:"noreferrer",className:"ml-1 text-violet-400/90 underline-offset-2 hover:underline",children:e},e)),J.length>4?"…":null]}):null]}):(0,n.jsx)("p",{className:"text-[11px] text-zinc-600",children:i("(日志与地址列表中暂无抖音房间)","(No Douyin room in log or URL list)")})]}),"not_found"!==m&&"invalid"!==m?(0,n.jsxs)("div",{className:"mt-4 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"text-[11px] font-semibold uppercase tracking-wider text-emerald-400/90",children:i("进程输出(stdout / 合并日志)","Process output")}),(0,n.jsx)("pre",{ref:A,className:"log-live-scroll mt-2 max-h-[min(22rem,42vh)] min-h-[12rem] overflow-auto whitespace-pre-wrap rounded-xl border border-emerald-500/20 bg-black/55 p-3 font-mono text-[10px] leading-relaxed text-emerald-100/95",children:p.trim()||i("(暂无)","(empty)")})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("div",{className:"text-[11px] font-semibold uppercase tracking-wider text-rose-400/90",children:i("异常信息(stderr)","Stderr")}),(0,n.jsx)("pre",{ref:D,className:"log-live-scroll mt-2 max-h-[min(22rem,42vh)] min-h-[12rem] overflow-auto whitespace-pre-wrap rounded-xl border border-rose-500/20 bg-black/55 p-3 font-mono text-[10px] leading-relaxed text-rose-100/95",children:g.trim()||i("(暂无)","(empty)")})]})]}):(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:i("进程未在 PM2 / 本地注册表中运行,日志区域已隐藏。","Process not running — log panes hidden.")})]})}function ey(e){return/tiktok/i.test(e.script)||/tiktok/i.test(e.pm2)}let ew=[{id:"720p60",labelZh:"720p @ 60Hz",labelEn:"720p @ 60Hz",spec:"1280x720, 59.94/60Hz, RGB/YUV 有限",hintZh:"板端 HDMI 输出时序与显示端一致;下游画布建议 1280\xd7720。",hintEn:"Match SoC HDMI timing to the display; downstream canvas e.g. 1280\xd7720."},{id:"1080p30",labelZh:"1080p @ 30Hz",labelEn:"1080p @ 30Hz",spec:"1920x1080, 30Hz",hintZh:"带宽紧张时可优先 1080p30;检查线材与接口规格。",hintEn:"Prefer 1080p30 when bandwidth is tight; check cable and port specs."},{id:"1080p60",labelZh:"1080p @ 60Hz",labelEn:"1080p @ 60Hz",spec:"1920x1080, 60Hz, 高刷转播",hintZh:"两端均需稳定支持 1080p60;长时间运行注意散热。",hintEn:"Both ends need stable 1080p60; mind thermals for long runs."}],eN="live-hub-tiktok-hdmi-check-v1",ej="live-hub-tiktok-hdmi-notes-v1",ek="live-hub-tiktok-source-mode-v1";function ez(e){var t;let{t:s,busy:l,entries:i,currentProc:r,setCurrentProc:a,urlConfig:o,setUrlConfig:c,onRunProcess:d,onSaveUrlConfig:x,onReplayGoLive:u,onCopy:m,fetchJson:h,apiPrefix:p,quickLinks:b,hdmiStatus:g,notify:f,onNavigate:v}=e,y=(0,_.useMemo)(()=>i.filter(ey),[i]),w=y.length?y:i,[N,j]=(0,_.useState)("srs"),[k,z]=(0,_.useState)([]),[S,C]=(0,_.useState)(!1),[R,P]=(0,_.useState)(!1),[L,M]=(0,_.useState)("1080p30"),[T,E]=(0,_.useState)("1080p30"),[I,D]=(0,_.useState)(!1),[Z,O]=(0,_.useState)(!0),[H,K]=(0,_.useState)(null),F=(0,_.useRef)(!1),W=(0,_.useRef)(o),[J,G]=(0,_.useState)(""),[V,q]=(0,_.useState)({}),X=(0,_.useCallback)(e=>{W.current=e,c(e)},[c]),$=(0,_.useCallback)(e=>{j(e);try{window.localStorage.setItem(ek,e)}catch(e){}},[]);(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(ek);("ffmpeg"===e||"replay"===e||"srs"===e)&&("ffmpeg"!==e&&"replay"!==e?j(e):j("srs"))}catch(e){}},[]),(0,_.useEffect)(()=>{F.current=!1},[r]),(0,_.useEffect)(()=>{W.current=o},[o]);let Q=(0,_.useCallback)(async()=>{if(r)try{var e;let t=new URLSearchParams({process:r,_ts:String(Date.now())}),s=await h("/get_url_config?".concat(t));X(null!==(e=s.content)&&void 0!==e?e:"")}catch(e){}},[X,r,h]);(0,_.useEffect)(()=>{if(!r||!Z||"ffmpeg"!==N)return;let e=window.setInterval(()=>{F.current||Q()},8e3);return()=>clearInterval(e)},[r,Z,Q,N]),(0,_.useEffect)(()=>{"ffmpeg"===N&&r&&Q()},[N,r,Q]);let ee=(0,_.useCallback)(async()=>{if(r){C(!0);try{var e;let t=await h("/tiktok_replay/list?process=".concat(encodeURIComponent(r)));z(null!==(e=t.files)&&void 0!==e?e:[])}catch(e){f(e.message),z([])}finally{C(!1)}}},[r,h,f]);(0,_.useEffect)(()=>{"replay"===N&&r&&ee()},[N,r,ee]),(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(eN);e&&q(JSON.parse(e))}catch(e){}try{G(window.localStorage.getItem(ej)||"")}catch(e){}},[]);let et=(0,_.useCallback)(e=>{q(e);try{window.localStorage.setItem(eN,JSON.stringify(e))}catch(e){}},[]),es=e=>{G(e);try{window.localStorage.setItem(ej,e)}catch(e){}},en=async()=>{if(r){D(!0),K(null),F.current=!1;try{var e;let t=new URLSearchParams({process:r,_ts:String(Date.now())}),n=await h("/get_url_config?".concat(t));X(null!==(e=n.content)&&void 0!==e?e:""),K(s("已从服务器读取 URL_config","Reloaded URL_config from server"))}catch(e){K(e.message)}finally{D(!1)}}},el=async()=>{try{await Promise.resolve(x(W.current)),F.current=!1}catch(e){}},ei=async e=>{if(r){P(!0);try{await h("/tiktok_replay/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({process:r,filename:e})}),f(s("已删除文件","File removed")),await ee()}catch(e){f(e.message)}finally{P(!1)}}},er=async e=>{var t;let n=null===(t=e.target.files)||void 0===t?void 0:t[0];if(e.target.value="",n&&r){P(!0);try{let e=new FormData;e.append("file",n);let t=await (0,Y.N)("".concat(p,"/tiktok_replay/upload?process=").concat(encodeURIComponent(r)),{method:"POST",body:e});f(t.message||s("上传成功","Uploaded")),await ee()}catch(e){f(e.message)}finally{P(!1)}}},ex=l||I||R,em=ew.find(e=>e.id===T),eh=!!(null==g?void 0:g.can_stream_hdmi),ep=(null==g?void 0:g.preferred_device)||(null==g?void 0:null===(t=g.video_devices)||void 0===t?void 0:t[0])||"";return(0,n.jsxs)("div",{className:"mx-auto max-w-4xl space-y-6",children:[v?(0,n.jsx)(eu,{t:s,title:s("TikTok HDMI 硬件链路","TikTok HDMI hardware chain"),subtitle:s("板端编码经 HDMI 到显示/下游设备;与「网络」页顶卡相同的箭头风格。","Encode on SoC → HDMI → display/downstream; same arrow style as Network header."),onNavigate:v,steps:[{label:"FFmpeg",sub:s("采集 / 推流","Capture"),gradient:"from-cyan-400 to-blue-500",target:"live_tiktok"},{label:"HDMI",sub:s("板载输出","SoC out"),gradient:"from-violet-500 to-fuchsia-500"},{label:"Android",sub:s("AOSP / 手机","AOSP"),gradient:"from-amber-400 to-orange-500",target:"android"}]}):null,(0,n.jsx)("div",{className:"rounded-2xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.14] via-zinc-950/80 to-fuchsia-500/10 p-6 shadow-xl shadow-cyan-900/10",children:(0,n.jsx)("div",{className:"flex flex-wrap items-start gap-4",children:(0,n.jsxs)("div",{className:"flex items-center gap-3",children:[(0,n.jsx)("div",{className:"flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-cyan-500/35 to-fuchsia-600/25 ring-1 ring-white/10",children:(0,n.jsx)(A.Z,{className:"h-5 w-5 text-cyan-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h2",{className:"text-lg font-semibold tracking-tight text-white",children:s("TikTok 无人直播(单路)","TikTok unmanned (single lane)")}),(0,n.jsx)("p",{className:"mt-0.5 text-xs text-zinc-500",children:s("布局对齐 YouTube 单频道:直播开关 + 源站 + URL_config;无 stream key,输出走 HDMI 硬件链路。","Same layout as YouTube single: controls + URLs; no stream key — HDMI hardware path.")})]})]})})}),(0,n.jsx)("div",{className:"rounded-2xl border border-cyan-500/20 bg-cyan-500/[0.06] p-4 ring-1 ring-white/[0.04]",children:(0,n.jsx)("p",{className:"text-xs leading-relaxed text-cyan-100/90",children:s("HDMI 互斥:后端在「开始 / 重新开始」TikTok 或 obs*.sh(SRS 拉流上屏)时,会自动停止其它正在占用 HDMI 链路的同类进程;与 YouTube 推流互不干扰。","HDMI mutex: starting TikTok or obs*.sh stops other TikTok/OBS HDMI sinks; YouTube RTMP is unaffected.")})}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/10 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-300"}),s("X86 HDMI 采集状态","X86 HDMI capture status")]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("这里直接显示主机是否识别到采集卡和 /dev/video 设备,可用于验证 ARM HDMI 进 X86 的链路。","Shows whether the host sees the capture card and /dev/video nodes for ARM-to-X86 HDMI ingest.")})]}),(0,n.jsx)("span",{className:"rounded-full px-3 py-1 text-[11px] font-semibold ring-1 ".concat(eh?"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-rose-500/25"),children:eh?s("可推流","Ready"):s("未就绪","Not ready")})]}),(0,n.jsxs)("div",{className:"mt-4 grid gap-3 md:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-4 text-[11px] text-zinc-400",children:[(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"device: "}),(0,n.jsx)("span",{className:"font-mono text-zinc-200",children:ep||"—"})]}),(0,n.jsxs)("p",{className:"mt-2",children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"video nodes: "}),(0,n.jsx)("span",{className:"font-mono text-zinc-200",children:((null==g?void 0:g.video_devices)||[]).join(", ")||"—"})]}),(0,n.jsxs)("p",{className:"mt-2",children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"usb matches: "}),(0,n.jsx)("span",{className:"text-zinc-300",children:((null==g?void 0:g.usb_capture_matches)||[]).length||0})]})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/25 p-4 text-[11px] text-zinc-400",children:[(0,n.jsx)("p",{className:"text-zinc-600",children:s("建议 FFmpeg","Suggested FFmpeg")}),(0,n.jsx)("p",{className:"mt-2 break-all font-mono text-zinc-200",children:(null==g?void 0:g.suggested_ffmpeg_cmd)||"—"}),(null==g?void 0:g.suggested_ffmpeg_cmd)?(0,n.jsxs)("button",{type:"button",onClick:()=>m(g.suggested_ffmpeg_cmd||""),className:"mt-3 inline-flex items-center gap-2 rounded-lg bg-cyan-500/15 px-3 py-2 text-xs font-medium text-cyan-100 ring-1 ring-cyan-500/25",children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),s("复制命令","Copy command")]}):null]})]}),((null==g?void 0:g.issues)||[]).length?(0,n.jsx)("ul",{className:"mt-4 list-inside list-disc space-y-1 text-[11px] text-amber-200/85",children:((null==g?void 0:g.issues)||[]).map(e=>(0,n.jsx)("li",{children:e},e))}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/20 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-amber-200/90",children:[(0,n.jsx)(B.Z,{className:"h-4 w-4"}),(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("直播开关","Live control")})]}),(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:s("TikTok 进程(PM2)","TikTok PM2 process")}),(0,n.jsx)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:r,onChange:e=>a(e.target.value),children:w.map(e=>(0,n.jsxs)("option",{value:e.pm2,children:[e.label," (",e.pm2,")"]},e.pm2))}),(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:s("直播源","Video source")}),(0,n.jsxs)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:N,onChange:e=>$(e.target.value),children:[(0,n.jsx)("option",{value:"ffmpeg",hidden:!0,children:"FFmpeg(抖音 / TikTok 等直播间 URL)"}),(0,n.jsx)("option",{value:"replay",hidden:!0,children:s("录播素材(本地上传)","VOD upload")}),(0,n.jsx)("option",{value:"srs",children:s("SRS(OBS 推流)","SRS (OBS)")})]}),"srs"===N?(0,n.jsx)("p",{className:"mt-2 text-[11px] leading-relaxed text-amber-200/85",children:s("此模式不显示 URL_config:请在 OBS 中填写下方 RTMP 推流地址;板端 SRS 拉流上屏仍可能占用 HDMI,与 TikTok 进程互斥规则不变。","URL_config is hidden: set the RTMP URL in OBS. SRS pull-to-HDMI may still mutex with TikTok per server rules.")}):null,(0,n.jsx)("div",{className:"mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3",children:[["start",s("开始直播","Start"),"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30"],["stop",s("停止直播","Stop"),"bg-rose-500/15 text-rose-200 ring-rose-500/25"],["restart",s("重新开始","Restart"),"bg-amber-500/15 text-amber-200 ring-amber-500/25"]].map(e=>{let[t,s,l]=e;return(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>d(t),className:"rounded-xl px-4 py-3 text-sm font-medium ring-1 ".concat(l),children:s},t)})}),(0,n.jsxs)("p",{className:"mt-3 text-center text-[11px] text-zinc-500",children:[s("当前 HDMI 预设(备忘)","HDMI preset (memo)"),":"," ",(0,n.jsx)("span",{className:"font-mono text-cyan-300/90",children:em?s(em.labelZh,em.labelEn):T})]})]}),"replay"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/25 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(eo.Z,{className:"h-4 w-4 text-violet-300"}),s("录播素材","VOD library")]}),(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,n.jsxs)("label",{className:"inline-flex cursor-pointer items-center gap-2 rounded-xl bg-violet-500/15 px-3 py-2 text-xs font-medium text-violet-100 ring-1 ring-violet-500/30",children:[(0,n.jsx)(ec.Z,{className:"h-3.5 w-3.5"}),(0,n.jsx)("span",{children:s("上传视频","Upload")}),(0,n.jsx)("input",{type:"file",accept:".mp4,.mkv,.ts,.flv,.mov,.m4v,.webm",className:"hidden",onChange:er})]}),(0,n.jsx)("button",{type:"button",disabled:ex||!r,onClick:()=>void ee(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-3 py-2 text-xs text-zinc-300",children:s("刷新列表","Refresh")})]})]}),(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:s("文件保存在服务器 config/tiktok_replay/对应进程目录;点「开始直播」会写入 replayfile 行并启动 PM2 进程。停播请用上方「停止直播」。","Files under config/tiktok_replay/. Go live writes replayfile + starts PM2. Use Stop above to stop.")}),S?(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:s("加载中…","Loading…")}):0===k.length?(0,n.jsx)("p",{className:"mt-4 text-xs text-zinc-500",children:s("暂无文件,请先上传。","No files yet.")}):(0,n.jsx)("ul",{className:"mt-4 space-y-2",children:k.map(e=>(0,n.jsxs)("li",{className:"flex flex-wrap items-center justify-between gap-2 rounded-xl border border-white/10 bg-black/30 px-3 py-2 text-xs text-zinc-300",children:[(0,n.jsx)("span",{className:"min-w-0 flex-1 truncate font-mono",children:e.name}),(0,n.jsxs)("span",{className:"shrink-0 text-zinc-500",children:[(e.size/1048576).toFixed(1)," MB"]}),(0,n.jsxs)("div",{className:"flex shrink-0 flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void(async()=>{try{await Promise.resolve(u(e.replay_url,e.name))}catch(e){}})(),className:"rounded-lg bg-emerald-500/20 px-2 py-1 text-[11px] text-emerald-100 ring-1 ring-emerald-500/30",children:s("开始直播","Go live")}),(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>d("stop"),className:"rounded-lg bg-rose-500/15 px-2 py-1 text-[11px] text-rose-100 ring-1 ring-rose-500/25",children:s("停播","Stop")}),(0,n.jsxs)("button",{type:"button",disabled:ex,onClick:()=>void ei(e.name),className:"inline-flex items-center gap-1 rounded-lg bg-zinc-700/40 px-2 py-1 text-[11px] text-zinc-200",children:[(0,n.jsx)(ed.Z,{className:"h-3 w-3"}),s("删除","Del")]})]})]},e.name))})]}):null,"srs"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-emerald-500/25 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("SRS / OBS 推流","SRS / OBS ingest")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("OBS:设置 → 推流 → 服务选「自定义」,服务器填 RTMP 地址;串流密钥可与 SRS 应用配置一致(默认 live/livestream)。","OBS → Stream → Custom: RTMP URL as server; stream key per SRS app (default live/livestream).")}),(0,n.jsxs)("dl",{className:"mt-4 space-y-3 text-xs",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("RTMP 推流地址(OBS 服务器)","RTMP publish URL")}),(0,n.jsxs)("dd",{className:"mt-1 flex flex-wrap items-center gap-2 font-mono text-emerald-200/90",children:[(0,n.jsx)("span",{className:"break-all",children:b.srs_rtmp_publish||"—"}),b.srs_rtmp_publish?(0,n.jsx)("button",{type:"button",className:"shrink-0 text-cyan-300 hover:text-cyan-200",onClick:()=>m(b.srs_rtmp_publish),children:(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"})}):null]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("HTTP-FLV 预览","HTTP-FLV preview")}),(0,n.jsxs)("dd",{className:"mt-1 flex flex-wrap items-center gap-2 font-mono text-zinc-400",children:[(0,n.jsx)("span",{className:"break-all",children:b.srs_play_flv||b.srs_http||"—"}),b.srs_play_flv||b.srs_http?(0,n.jsx)("button",{type:"button",className:"shrink-0 text-cyan-300 hover:text-cyan-200",onClick:()=>m(b.srs_play_flv||b.srs_http),children:(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"})}):null]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-500",children:s("SRS 控制台","SRS web UI")}),(0,n.jsx)("dd",{className:"mt-1 font-mono text-zinc-400",children:(0,n.jsx)("a",{href:b.srs_http||"#",target:"_blank",rel:"noreferrer",className:"text-cyan-300 hover:underline",children:b.srs_http||"—"})})]})]})]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-400"}),s("HDMI 硬件与时序","HDMI timing")]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("点选一路作为现场备忘;与板端 ffmpeg/OBS 采集参数对齐。","Pick a preset as field memo; match capture settings.")}),(0,n.jsx)("div",{className:"mt-4 grid gap-3 md:grid-cols-3",children:ew.map(e=>{let t=L===e.id,l=T===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>{M(t?null:e.id),E(e.id)},className:"rounded-2xl border p-4 text-left transition ".concat(l?"border-cyan-500/50 ring-1 ring-cyan-500/20":""," ").concat(t?"border-cyan-500/40 bg-cyan-500/10":"border-white/[0.08] bg-zinc-950/50 hover:border-white/15"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(ea.Z,{className:"h-4 w-4 text-cyan-400"}),s(e.labelZh,e.labelEn)]}),(0,n.jsx)("p",{className:"mt-2 font-mono text-[11px] text-zinc-400",children:e.spec}),t?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:s(e.hintZh,e.hintEn)}):null]},e.id)})}),(0,n.jsxs)("div",{className:"mt-6 rounded-xl border border-white/5 bg-black/25 p-4",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-zinc-400",children:s("上线前检查清单(本地保存)","Pre-flight checklist (local)")}),(0,n.jsx)("ul",{className:"mt-3 space-y-2",children:[{id:"cable",zh:"HDMI 线材插紧、方向正确(如需转接头确认规格)",en:"HDMI cable seated; adapter specs OK"},{id:"thermal",zh:"长时间推流注意散热,过热会掉帧黑屏",en:"Thermal headroom for long runs"}].map(e=>(0,n.jsxs)("li",{className:"flex items-start gap-2 text-xs text-zinc-400",children:[(0,n.jsx)("input",{type:"checkbox",className:"mt-0.5 rounded border-white/20 bg-black/40",checked:!!V[e.id],onChange:t=>et({...V,[e.id]:t.target.checked})}),(0,n.jsx)("span",{children:s(e.zh,e.en)})]},e.id))}),(0,n.jsx)("label",{className:"mt-4 block text-[11px] text-zinc-500",children:s("HDMI / 走线备忘","HDMI / wiring notes")}),(0,n.jsx)("textarea",{className:"mt-1 min-h-[72px] w-full resize-y rounded-lg border border-white/10 bg-black/40 p-3 text-xs text-zinc-300",value:J,onChange:e=>es(e.target.value),spellCheck:!1,placeholder:s("接口、线长、显示器型号…","Ports, cable length, display model…")})]})]}),"ffmpeg"===N?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:s("直播地址列表 \xb7 URL_config.ini","URL_config.ini")}),(0,n.jsxs)("button",{type:"button",className:"inline-flex items-center gap-1 text-xs text-cyan-300 hover:text-cyan-200",onClick:()=>m(o),children:[(0,n.jsx)(U.Z,{className:"h-3.5 w-3.5"}),s("复制全文","Copy all")]})]}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:s("一行一个地址;可勾选自动从服务器刷新,或点「重新读取」手动同步。","One URL per line; enable auto-refresh or reload from server.")}),(0,n.jsx)("div",{className:"mt-2 flex flex-wrap items-center gap-4 text-xs text-zinc-400",children:(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:Z,onChange:e=>O(e.target.checked)}),s("每 8 秒自动从服务器刷新","Auto-refresh from server every 8s")]})}),H?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:H}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:o,onChange:e=>{F.current=!0,X(e.target.value)},spellCheck:!1}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void en(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-300",children:s("重新读取","Reload from server")}),(0,n.jsx)("button",{type:"button",disabled:ex,onClick:()=>void el(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm font-medium text-cyan-100 ring-1 ring-cyan-500/30",children:s("保存 URL 配置","Save URL config")})]})]}):null,(0,n.jsx)(ev,{t:s,currentProc:r,fetchJson:h,urlListText:"ffmpeg"===N?o:"",keySuffixUi:"",showKeyRow:!1})]})}var eS=s(5468),e_=s(9397),eC=s(90);let eR="live-hub-youtube-pro-v2",eP=/[^a-zA-Z0-9_.-]+/g;function eL(e){return(e||"").trim().replace(eP,"_")}function eM(e){return eL(String(e.pm2Name||""))||eO(String(e.id||""))}function eT(e){if(!e||"object"!=typeof e)return null;let t=String(e.id||"").trim();if(!t)return null;let s=String(e.name||"").trim()||t;return{id:t,name:s,streamKeySuffix:"string"==typeof e.streamKeySuffix?e.streamKeySuffix:void 0,streamKey:"string"==typeof e.streamKey?e.streamKey:void 0,urlLines:"string"==typeof e.urlLines?e.urlLines:void 0,pm2Name:"string"==typeof e.pm2Name?e.pm2Name:void 0,notes:"string"==typeof e.notes?e.notes:void 0}}function eU(e){if(!e)return[];try{let t=JSON.parse(e);if(!Array.isArray(t))return[];return t.map(e=>eT(e)).filter(e=>null!==e)}catch(e){return[]}}function eE(e){window.localStorage.setItem(eR,JSON.stringify(e))}let eI=["/hub/youtube_pro_channels","/youtube_pro_channels"];async function eA(e){for(let s of eI)try{var t;let n=await e(s);return{fetched:!0,channels:(t=n.channels,Array.isArray(t)?t.map(e=>eT(e)).filter(e=>null!==e):[])}}catch(e){}return{fetched:!1,channels:[]}}async function eD(e,t){let s=null;for(let n of eI)try{await e(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channels:t})});return}catch(e){s=e instanceof Error?e:Error(String(e))}throw null!=s?s:Error("youtube_pro_channels sync failed")}function eZ(){return"ch_".concat(Date.now().toString(36),"_").concat(Math.random().toString(36).slice(2,7))}function eO(e){return"youtube2__".concat(eL(e)||"channel")}function eH(e){return/youtube/i.test(e.script)||/youtube/i.test(e.pm2)}function eB(e){return(e.split("/").pop()||e).replace(/\.[^.]+$/,"")}function eK(e){return eH(e)&&e.pm2.trim()===eB(e.script).trim()}function eF(e){var t;return!!e&&(/youtube/i.test(e.pm2)||/youtube/i.test(null!==(t=e.script)&&void 0!==t?t:""))}function eW(e,t){let s=((null==t?void 0:t.business_status)||"").trim().toLowerCase();return(null==t?void 0:t.is_pushing)||"streaming"===s?0:eV(null==t?void 0:t.process_status)?1:"youtube"===e.pm2.trim().toLowerCase()?2:"youtube"===eB(e.script).trim().toLowerCase()?3:10}let eY="live-hub-youtube-mode-v1",eJ=new Set(["tiktok","obs","web"]);function eG(e){var t,s,n;return{name:e.name,streamKey:null!==(t=e.streamKey)&&void 0!==t?t:"",pm2Name:null!==(s=e.pm2Name)&&void 0!==s?s:eO(e.id),notes:null!==(n=e.notes)&&void 0!==n?n:""}}function eV(e){return/^(online|running)$/i.test((e||"").trim())}function eq(e,t){let s=((null==e?void 0:e.business_status)||"").trim().toLowerCase();return"ready"===s?t("已配置","Ready"):"streaming"===s?t("推流中","Pushing"):"waiting_source"===s?t("等待上游","Waiting source"):"source_live"===s?t("源已开播,转推未起","Source live, relay down"):"monitoring"===s?t("监控中","Monitoring"):"misconfigured"===s?t("密钥异常","Key invalid"):"stale"===s?t("心跳过期","Stale"):"error"===s?t("异常","Error"):eV(null==e?void 0:e.process_status)?t("运行中","Running"):((null==e?void 0:e.process_status)||"").trim()||t("未运行","Idle")}function eX(e){return e.split(/\r?\n/).map(e=>e.trim()).filter(e=>e&&!e.startsWith("#"))}function e$(e,t){return eX(e).length>0&&0===eX(t).length}function eQ(e){let{kind:t,busyKind:s,targetPm2:l,busy:i,online:r,label:a,className:o,baseLock:c,onRun:d,notify:x,t:u,compact:m}=e,h=function(e){if(!(null==e?void 0:e.startsWith("proc:")))return null;let t=e.slice(5),s=t.indexOf(":");if(s<=0)return null;let n=t.slice(0,s),l=t.slice(s+1);return l&&["start","stop","restart"].includes(n)?{action:n,pm2:l}:null}(i),p=h&&h.pm2===l,b=null!=s?s:t,g=!!(p&&h.action===b),f=!!(p&&h.action!==b),y="start"===t&&r,w="stop"===t&&!r,N="restart"===t&&!r,j=y||w||N;return(0,n.jsxs)("button",{type:"button",onClick:()=>{if(!g){if(f){x(u("请等待当前操作完成","Wait for the current action to finish"));return}if(y){x(u("已在直播中,无需重复开始","Already live"));return}if(w){x(u("当前未在直播,无法停止","Not live — cannot stop"));return}if(N){x(u("进程未运行时请使用「开始直播」","Use Start when the process is not running"));return}c||d()}},className:"relative overflow-hidden rounded-xl ring-1 transition ".concat(m?"min-h-[2rem] px-3 py-1.5 text-xs font-medium":"min-h-[2.75rem] px-4 py-3 text-sm font-medium"," ").concat(o," ").concat(j||c||g||f?"cursor-not-allowed":""," ").concat(j&&!g?"opacity-45":""," ").concat(f?"opacity-35":""," ").concat(g?"ring-violet-400/40":""),children:[g?(0,n.jsx)("span",{className:"absolute inset-0 flex items-center justify-center bg-black/25 backdrop-blur-[2px]",children:(0,n.jsx)(v.Z,{className:"".concat(m?"h-4 w-4":"h-6 w-6"," animate-spin text-white")})}):null,(0,n.jsxs)("span",{className:"inline-flex items-center justify-center gap-1.5 ".concat(g?"invisible":""),children:[j?(0,n.jsx)(eS.Z,{className:"".concat(m?"h-3 w-3":"h-4 w-4"," shrink-0 opacity-90"),"aria-hidden":!0}):null,a]})]})}function e0(e){var t,s,l,i,r;let{t:a,busy:o,entries:d,currentProc:x,setCurrentProc:u,urlConfig:m,setUrlConfig:h,onRunProcess:p,onSaveUrlConfig:b,fetchJson:g,liveProcesses:f,notify:w,onNavigate:N,quickLinks:j}=e,k=(0,_.useMemo)(()=>d.filter(eH),[d]),z=(0,_.useMemo)(()=>k.filter(e=>eK(e)),[k]),S=(0,_.useMemo)(()=>(function(e,t){let s=e.filter(eH),n=s.filter(e=>eK(e)),l=n.length?[...n]:s.length?[...s]:[...e],i=new Set(l.map(e=>e.pm2.trim()));for(let e of t.filter(eF)){var r,a,o,c;let t=e.pm2.trim();if(!t||i.has(t))continue;let n=(e.business_status||"").trim().toLowerCase();if(!e.is_pushing&&"streaming"!==n&&!eV(e.process_status))continue;let d=s.find(e=>e.pm2.trim()===t);l.push(null!=d?d:{pm2:t,script:null!==(o=null!==(a=e.script)&&void 0!==a?a:null===(r=s[0])||void 0===r?void 0:r.script)&&void 0!==o?o:"youtube.py",label:null!==(c=e.label)&&void 0!==c?c:t}),i.add(t)}return l})(k.length?k:d,f),[d,f,k]),C=(0,_.useMemo)(()=>(function(e,t){if(!e.length)return"";let s=new Map(t.filter(eF).map(e=>[e.pm2.trim(),e])),n=e[0],l=eW(n,s.get(n.pm2.trim()));for(let t of e.slice(1)){let e=eW(t,s.get(t.pm2.trim()));e"youtube"===e.pm2.trim().toLowerCase());if(n)return n.pm2;let l=e.find(e=>"youtube"===eB(e.script).trim().toLowerCase());return l?l.pm2:null!==(s=null===(t=e[0])||void 0===t?void 0:t.pm2)&&void 0!==s?s:""}(S),[f,S]),[R,P]=(0,_.useState)("single"),[L,M]=(0,_.useState)([]),[T,U]=(0,_.useState)(""),[E,I]=(0,_.useState)(""),[A,D]=(0,_.useState)({key:"",rtmps:!0,bitrate:"",fastAudio:!1}),[Z,O]=(0,_.useState)(null),[H,K]=(0,_.useState)(!1),[F,W]=(0,_.useState)(""),[Y,J]=(0,_.useState)(!0),[G,V]=(0,_.useState)(!1),[q,X]=(0,_.useState)(!1),[$,Q]=(0,_.useState)(null),[ee,et]=(0,_.useState)({}),[es,en]=(0,_.useState)(!1),[el,ei]=(0,_.useState)(null),[er,ea]=(0,_.useState)(0),eo=(0,_.useRef)(!1),ec=(0,_.useRef)(m),ef=(0,_.useRef)(F),ey=(0,_.useRef)(x),ew=(0,_.useRef)(0),eN=(0,_.useRef)(0),ej=(0,_.useCallback)(e=>{ec.current=e,h(e)},[h]),ek=(0,_.useCallback)(e=>{ef.current=e,W(e)},[]);(0,_.useEffect)(()=>{eo.current=!1},[x]),(0,_.useEffect)(()=>{ey.current=x},[x]),(0,_.useEffect)(()=>{ec.current=m},[m]),(0,_.useEffect)(()=>{ef.current=F},[F]);let ez=(0,_.useCallback)(async()=>{let e=ey.current.trim();if(!e)return;let t=++ew.current;try{var s,n,l;let i=new URLSearchParams({process:e,_ts:String(Date.now())}),r=await g("/get_url_config?".concat(i));if(t!==ew.current||e!==ey.current.trim())return;let a=null!==(s=r.content)&&void 0!==s?s:"";if(eo.current)return;if("single"===R)ej(a);else{let e=null!==(l=null===(n=L.find(e=>e.id===T))||void 0===n?void 0:n.urlLines)&&void 0!==l?l:"";if(e$(e,a)){ek(e);return}ek(a),T&&M(e=>{let t=e.map(e=>e.id===T?{...e,urlLines:a}:e);return eE(t),t})}}catch(e){}},[T,ek,ej,L,g,R]);(0,_.useEffect)(()=>{if(!x||!Y)return;let e=window.setInterval(()=>{eo.current||ez()},8e3);return()=>clearInterval(e)},[x,Y,ez]);let eS=async()=>{let e=ey.current.trim();if(!e)return;V(!0),Q(null),eo.current=!1;let t=++ew.current;try{var s,n,l;let i=new URLSearchParams({process:e,_ts:String(Date.now())}),r=await g("/get_url_config?".concat(i));if(t!==ew.current||e!==ey.current.trim())return;let o=null!==(s=r.content)&&void 0!==s?s:"";if(eo.current){Q(a("检测到本地未保存修改,已保留当前草稿","Kept the local unsaved draft"));return}if("single"===R)ej(o);else{let e=null!==(l=null===(n=L.find(e=>e.id===T))||void 0===n?void 0:n.urlLines)&&void 0!==l?l:"";if(e$(e,o)){ek(e),Q(a("服务器暂无已保存 URL,已保留本地草稿","Server has no saved URLs, kept the local draft"));return}ek(o),T&&M(e=>{let t=e.map(e=>e.id===T?{...e,urlLines:o}:e);return eE(t),t})}Q(a("已从服务器读取 URL_config","Reloaded URL_config from server"))}catch(s){if(t!==ew.current||e!==ey.current.trim())return;Q(s.message)}finally{t===ew.current&&e===ey.current.trim()&&V(!1)}};(0,_.useEffect)(()=>{try{let e=window.localStorage.getItem(eY);("pro"===e||"single"===e)&&P(e)}catch(e){}},[]),(0,_.useEffect)(()=>{let e=!1;return(async()=>{try{let t=await eA(g);if(t.fetched&&t.channels.length&&!e){M(t.channels),eE(t.channels);return}}catch(e){}if(e)return;let t=function(){let e=window.localStorage.getItem(eR);if(!e&&(e=window.localStorage.getItem("live-hub-youtube-pro-v1"))){let t=eU(e);if(t.length)return window.localStorage.setItem(eR,JSON.stringify(t)),t}return eU(e)}(),s=t.length>0?t:(()=>{let e=eZ();return[{id:e,name:"线路 1",streamKey:"",urlLines:"",pm2Name:eO(e),notes:""}]})();M(s),t.length||eE(s)})(),()=>{e=!0}},[g]),(0,_.useEffect)(()=>{if(!L.length){T&&U("");return}T&&L.some(e=>e.id===T)||U(L[0].id)},[L,T]);let eP=(0,_.useCallback)(e=>f.find(t=>t.pm2===e),[f]),eT=(0,_.useCallback)(e=>{var t;return null===(t=eP(e))||void 0===t?void 0:t.process_status},[eP]),eI=eP(x),e0=(0,_.useMemo)(()=>{var e,t;return!!x&&(/youtube/i.test(x)||/youtube/i.test(null!==(e=null==eI?void 0:eI.script)&&void 0!==e?e:"")||/youtube/i.test(null!==(t=null==eI?void 0:eI.label)&&void 0!==t?t:""))},[x,eI]);(0,_.useEffect)(()=>{"single"!==R||!C||x&&e0||u(C)},[x,e0,R,C,u]);let e1=(0,_.useCallback)(e=>{M(e),eE(e)},[]),e5=(0,_.useCallback)(async e=>{await eD(g,e)},[g]),e2=(0,_.useCallback)(e=>{e1(e),e5(e).catch(e=>{w("".concat(a("多频道列表未同步到服务器","Channel list not saved on server"),": ").concat(e.message))})},[e1,w,e5,a]),e3=L.find(e=>e.id===T)||L[0],e4=(0,_.useMemo)(()=>{var e;return e3?null!==(e=ee[e3.id])&&void 0!==e?e:eG(e3):null},[e3,ee]);(0,_.useEffect)(()=>{e3&&et(e=>e[e3.id]?e:{...e,[e3.id]:eG(e3)})},[e3]),(0,_.useEffect)(()=>{ei(null)},[T]);let e6=(0,_.useCallback)((e,t)=>{e3&&(et(s=>{var n;return{...s,[e3.id]:{...null!==(n=s[e3.id])&&void 0!==n?n:eG(e3),[e]:t}}}),ei(null))},[e3]),e8=e3?eM(e3):"",e9=e3&&e4?eM({id:e3.id,pm2Name:e4.pm2Name}):"",e7=!!e3&&!!e4&&e9!==e8,te=(0,_.useMemo)(()=>{if(!e3||!e4)return{ok:!1,message:a("请选择线路","Pick a lane"),normalizedName:"",normalizedPm2:"",normalizedStreamKey:"",normalizedNotes:""};let e=e4.name.trim()||"".concat(a("线路","Ch")," ").concat(L.findIndex(e=>e.id===e3.id)+1),t=eM({id:e3.id,pm2Name:e4.pm2Name});return t?eJ.has(t)?{ok:!1,message:a("该进程名保留给系统模块,请换一个","This PM2 name is reserved"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e4.streamKey.trim(),normalizedNotes:e4.notes.trim()}:L.find(e=>e.id!==e3.id&&eM(e)===t)?{ok:!1,message:a("该进程名已被其它线路占用","Another lane already uses this PM2 name"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e4.streamKey.trim(),normalizedNotes:e4.notes.trim()}:{ok:!0,message:"",normalizedName:e,normalizedPm2:t,normalizedStreamKey:e4.streamKey.trim(),normalizedNotes:e4.notes.trim()}:{ok:!1,message:a("进程名不能为空","PM2 name is required"),normalizedName:e,normalizedPm2:t,normalizedStreamKey:e4.streamKey.trim(),normalizedNotes:e4.notes.trim()}},[e3,e4,L,a]),tt=(0,_.useMemo)(()=>{if(!e3||!e4)return!1;let e=eG(e3);return e4.name!==e.name||e4.streamKey!==e.streamKey||eL(e4.pm2Name)!==eL(e.pm2Name)||e4.notes!==e.notes},[e3,e4]),ts=(0,_.useCallback)(e=>{var t,s,n;if("single"===e&&"pro"===R&&e3){let e=ef.current;e!==(null!==(t=e3.urlLines)&&void 0!==t?t:"").trim()&&e2(L.map(t=>t.id===e3.id?{...t,urlLines:e}:t))}if("pro"===e&&L.length){let e=L[0],t=ec.current.trim(),l=z.some(e=>e.pm2===x)&&x.trim()?x.trim():eM(e),i=[{...e,streamKey:A.key.trim()||e.streamKey||"",urlLines:t||e.urlLines||"",pm2Name:l},...L.slice(1)];e2(i);let r=null!==(s=i.find(e=>e.id===T))&&void 0!==s?s:i[0];ek((null!==(n=null==r?void 0:r.urlLines)&&void 0!==n?n:"").trim()||t)}P(e);try{window.localStorage.setItem(eY,e)}catch(e){}},[R,e3,L,ek,A.key,x,z,T,e2]),tn=(0,_.useCallback)(e=>{var t,s;let n=ef.current;e3&&"pro"===R&&n!==(null!==(t=e3.urlLines)&&void 0!==t?t:"").trim()&&e2(L.map(e=>e.id===e3.id?{...e,urlLines:n}:e)),ek((null!==(s=e.urlLines)&&void 0!==s?s:"").trim()),U(e.id),u(eM(e))},[e3,ek,R,L,u,e2]);(0,_.useEffect)(()=>{if("pro"!==R||!T)return;let e=L.find(e=>e.id===T);e&&u(eM(e))},[R,T,L,u]);let tl=(0,_.useCallback)(async()=>{let e=ey.current.trim();if(!e)return;let t=++eN.current;K(!0),O(null);try{var s;let n=new URLSearchParams({process:e,_ts:String(Date.now())}),l=await g("/get_config?".concat(n));if(t!==eN.current||e!==ey.current.trim())return;let i=null!==(s=l.content)&&void 0!==s?s:"";I(i),D(ep(i))}catch(s){if(t!==eN.current||e!==ey.current.trim())return;O(s.message)}finally{t===eN.current&&e===ey.current.trim()&&K(!1)}},[g]);(0,_.useEffect)(()=>{x&&(ez(),tl(),ea(e=>e+1))},[T,x,tl,R,ez]);let ti=(0,_.useCallback)(async()=>{var e;if(!e3||!e4)throw Error(a("请选择线路","Pick a lane"));if(!te.ok)throw Error(te.message);let t=ef.current,s=L.map(e=>e.id===e3.id?{...e,name:te.normalizedName,streamKey:te.normalizedStreamKey,pm2Name:te.normalizedPm2,notes:te.normalizedNotes,urlLines:t}:e);return e1(s),await e5(s),et(e=>({...e,[e3.id]:{name:te.normalizedName,streamKey:te.normalizedStreamKey,pm2Name:te.normalizedPm2,notes:te.normalizedNotes}})),{next:s,savedLane:null!==(e=s.find(e=>e.id===e3.id))&&void 0!==e?e:{...e3,name:te.normalizedName,streamKey:te.normalizedStreamKey,pm2Name:te.normalizedPm2,notes:te.normalizedNotes,urlLines:t},pm2:te.normalizedPm2}},[e3,e4,te,e1,L,e5,a]),tr=(0,_.useCallback)(async()=>{if(e3){en(!0),ei(null);try{let e=await ti();u(e.pm2),ei(a("线路设置已保存","Lane settings saved"))}catch(t){let e=t.message;ei(e),w(e)}finally{en(!1)}}},[e3,w,ti,u,a]),ta=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:ey.current,s=t.trim();if(!s){O(a("请选择进程","Pick a process"));return}K(!0),O(null);try{let t=new URLSearchParams({process:s});await g("/save_config?".concat(t),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:e})}),I(e),O(a("youtube.ini 已保存","youtube.ini saved"))}catch(e){O(e.message)}finally{K(!1)}},to=async()=>{let e=eg(A);await ta(e)},tc=async()=>{K(!0),O(null);try{var e,t;let s=E||eg(A),n=await g("/config_optimize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({root:"app-config",path:(t=ey.current,"youtube.".concat(t.replace(/[^a-zA-Z0-9_.-]/g,"_"),".ini")),content:s,action:"youtube_balanced_preset"})}),l=null!==(e=n.content)&&void 0!==e?e:s;I(l),D(ep(l)),O(n.message||a("已应用 YouTube 预设","Applied YouTube preset"))}catch(e){O(e.message)}finally{K(!1)}},td=(0,_.useMemo)(()=>{var e,t;if("single"===R)return em("key = ".concat(A.key));let s=null!==(t=null!==(e=null==e4?void 0:e4.streamKey)&&void 0!==e?e:null==e3?void 0:e3.streamKey)&&void 0!==t?t:"";return em("key = ".concat(s))},[R,A.key,null==e3?void 0:e3.streamKey,null==e4?void 0:e4.streamKey]),tx=async()=>{if(e3)try{en(!0),ei(null),ew.current+=1;let e=ef.current,t=await ti();u(t.pm2),await Promise.resolve(b(e,t.pm2)),eo.current=!1,ei(a("线路与地址已保存","Lane settings and URLs saved"))}catch(t){let e=t instanceof Error?t.message:String(t);ei(e),w(e)}finally{en(!1)}},tu=async()=>{let e=ey.current.trim();if(!e){w(a("请选择进程","Pick a process"));return}try{ew.current+=1,await Promise.resolve(b(ec.current,e)),eo.current=!1}catch(e){}},tm=async()=>{let e="single"===R?ec.current:ef.current;X(!0),Q(null);try{var t;let s=await g("/config_optimize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({root:"app-config",path:eb(ey.current),content:e,action:"dedupe_urls"})}),n=null!==(t=s.content)&&void 0!==t?t:e;"single"===R?ej(n):ek(n),eo.current=!0,Q(s.message||a("已去重当前 URL 列表","Deduplicated current URL list"))}catch(e){Q(e.message)}finally{X(!1)}},th=async()=>{if(!e3||!e4)return;let e=ep(E||eg(A)),t=eg({key:e4.streamKey.trim(),rtmps:e.rtmps,bitrate:e.bitrate,fastAudio:e.fastAudio});en(!0),ei(null);try{let e=await ti();u(e.pm2),await ta(t,e.pm2),ei(a("线路设置与 youtube.ini 已保存","Lane settings and youtube.ini saved"))}catch(t){let e=t.message;ei(e),w(e)}finally{en(!1)}},tp="single"===R?m:F,tb=(0,_.useMemo)(()=>eX(tp).length,[tp]),tg=(0,_.useMemo)(()=>eh(tp).length,[tp]),tf=(0,_.useMemo)(()=>(0,eC.O0)(j),[j]),tv=(0,_.useMemo)(()=>(0,eC.U_)(tf,"pro"===R&&e3?L.findIndex(e=>e.id===e3.id):0),[e3,L,R,tf]),ty=tv.link?(0,ex.p)(tv.link.url):(0,ex.p)("http://live.local:9200/"),tw=(0,_.useMemo)(()=>(0,eC.Tc)(tf),[tf]),tN=(0,_.useMemo)(()=>{var e;return[null===(e=tv.link)||void 0===e?void 0:e.url,null==j?void 0:j.chromium_remote_debug,null==j?void 0:j.srs_api].filter(e=>!!e)},[j,null===(t=tv.link)||void 0===t?void 0:t.url]),tj=(0,_.useCallback)(async e=>{let t=e3?eM(e3):"";return e3&&e===t?await ti():(e1(L),await e5(L),{next:L,savedLane:null!=e3?e3:null,pm2:e})},[e3,e1,L,ti,e5]),tk=(0,_.useCallback)(async(e,t)=>{let s=!!e3&&eM(e3)===t;if("stop"!==e){var n,l;let e=L,s=t;try{let n=await tj(t);e=n.next,s=n.pm2}catch(e){w("".concat(a("当前线路保存失败","Failed to save the active lane"),": ").concat(e.message));return}let i=e.find(e=>eM(e)===s),r=null!==(n=null==i?void 0:i.urlLines)&&void 0!==n?n:"";if(!(null!==(l=null==i?void 0:i.streamKey)&&void 0!==l?l:"").trim()){w(a("请先填写当前线路的推流密钥","Fill the stream key before starting"));return}if(!eX(r).length){w(a("请先填写当前线路的直播地址","Fill the source URLs before starting"));return}t=s}s&&u(t),p(e,t)},[e3,L,tj,w,p,u,a]),tz=!!o||H||G||q||es,tS=eV(eT(x)),t_=(0,n.jsxs)("div",{className:"mt-2 flex flex-wrap items-center gap-4 text-xs text-zinc-400",children:[(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:Y,onChange:e=>J(e.target.checked)}),a("每 8 秒自动从服务器刷新","Auto-refresh from server every 8s")]}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void eS(),className:"rounded-lg border border-white/10 px-3 py-1.5 text-zinc-200 hover:bg-white/5",children:a("手动重新读取","Reload from server")}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void tm(),className:"rounded-lg border border-cyan-500/20 bg-cyan-500/10 px-3 py-1.5 text-cyan-100 hover:bg-cyan-500/15",children:q?a("去重中...","Deduplicating..."):a("智能去重","Deduplicate")})]});return(0,n.jsxs)("div",{className:"mx-auto max-w-6xl space-y-6",children:[N?(0,n.jsx)(eu,{t:a,title:a("YouTube 转播链路","YouTube relay chain"),subtitle:a("与「网络」页相同风格的链路示意:代理 → 编码推流 → 浏览器侧操作工作室。","Same visual language as Network: proxy → encode → browser studio."),onNavigate:N,steps:[{label:"ShellCrash",sub:a("透明代理","Proxy"),gradient:"from-violet-500 to-fuchsia-500",target:"network"},{label:"FFmpeg",sub:a("推流 / 转码","Stream"),gradient:"from-cyan-400 to-blue-500",target:"live_youtube"},{label:"Neko",sub:tv.link?"".concat(a("Chromium 工作室","Chromium studio")," \xb7 ").concat(tv.link.label):a("Chromium 工作室","Chromium studio"),gradient:"from-emerald-400 to-teal-500",openUrl:ty}]}):null,(0,n.jsx)("div",{className:"rounded-2xl border border-violet-500/25 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/80 to-cyan-500/5 p-6 shadow-xl shadow-violet-900/10",children:(0,n.jsxs)("div",{className:"flex flex-wrap items-start justify-between gap-4",children:[(0,n.jsxs)("div",{className:"flex items-center gap-3",children:[(0,n.jsx)("div",{className:"flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-red-500/30 to-violet-600/30 ring-1 ring-white/10",children:(0,n.jsx)(c.Z,{className:"h-5 w-5 text-red-200"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("h2",{className:"text-lg font-semibold tracking-tight text-white",children:a("YouTube 无人直播","YouTube unmanned live")}),(0,n.jsx)("p",{className:"mt-0.5 text-xs text-zinc-500",children:a("单路一条流;多路时每条线路独立保存自己的密钥、URL 列表和 PM2 任务。填好后保存,再点「开始」。Neko 浏览器仅作辅助工作室,不再阻塞推流。","Single lane or multi-lane: every lane keeps its own key, URL list, and PM2 task. Save, then Start. Neko is optional browser tooling and no longer blocks streaming.")})]})]}),(0,n.jsxs)("div",{className:"flex rounded-xl border border-white/10 bg-black/30 p-1 ".concat(""),"aria-hidden":!1,children:[(0,n.jsx)("button",{type:"button",onClick:()=>ts("single"),className:"rounded-lg px-4 py-2 text-xs font-medium transition ".concat("single"===R?"bg-violet-500/25 text-white":"text-zinc-500 hover:text-zinc-300"),children:a("单频道","Single")}),(0,n.jsx)("button",{type:"button",onClick:()=>ts("pro"),className:"rounded-lg px-4 py-2 text-xs font-medium transition ".concat("pro"===R?"bg-violet-500/25 text-white":"text-zinc-500 hover:text-zinc-300"),children:a("多频道 Pro","Multi Pro")})]})]})}),tN.length?(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[tv.link?(0,n.jsx)("a",{href:ty,target:"_blank",rel:"noreferrer",className:"rounded-lg border border-emerald-500/20 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-100 ring-1 ring-emerald-500/20",children:a("打开当前线路 Neko 工作室","Open lane Neko studio")}):null,tf.length>1?(0,n.jsx)("div",{className:"flex flex-wrap gap-2",children:tf.map(e=>{var t;return(0,n.jsxs)("a",{href:(0,ex.p)(e.url),target:"_blank",rel:"noreferrer",className:"rounded-lg border px-3 py-2 text-xs ring-1 ".concat((null===(t=tv.link)||void 0===t?void 0:t.id)===e.id?"border-emerald-400/30 bg-emerald-500/15 text-emerald-50 ring-emerald-400/25":"border-white/10 bg-white/5 text-zinc-200 ring-white/10"),children:[e.label,e.port?" :".concat(e.port):""]},e.key)})}):null,(null==j?void 0:j.chromium_remote_debug)?(0,n.jsx)("a",{href:(0,ex.p)(j.chromium_remote_debug),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-cyan-500/20 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100 ring-1 ring-cyan-500/20",children:a("打开 Chromium 调试","Open Chromium debug")}):null,(null==j?void 0:j.srs_api)?(0,n.jsx)("a",{href:(0,ex.p)(j.srs_api),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-violet-500/20 bg-violet-500/10 px-3 py-2 text-xs text-violet-100 ring-1 ring-violet-500/20",children:a("打开 SRS API","Open SRS API")}):null]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/20 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]",children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 text-amber-200/90",children:[(0,n.jsx)(B.Z,{className:"h-4 w-4"}),(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播开关","Live control")})]}),"single"===R?(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)("label",{className:"mt-4 block text-xs font-semibold uppercase tracking-wider text-zinc-500",children:a("转播任务","Relay task")}),(0,n.jsxs)("select",{className:"mt-2 w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white",value:x,onChange:e=>{let t=e.target.value;ey.current=t,u(t)},children:[x&&!S.some(e=>e.pm2===x)?(0,n.jsxs)("option",{value:x,children:[x,eI?" (".concat(eq(eI,a),")"):""]}):null,S.map(e=>(0,n.jsxs)("option",{value:e.pm2,children:[function(e,t){let s=eB(e.script).toLowerCase();return s.startsWith("douyin_youtube")?t("抖音→YouTube","Douyin → YouTube"):s.startsWith("youtube")?t("YouTube","YouTube"):e.label}(e,a)," (",e.pm2,")"]},e.pm2))]}),(0,n.jsxs)("div",{className:"mt-4 grid grid-cols-2 gap-2 sm:grid-cols-3",children:[(0,n.jsx)(eQ,{kind:"start",targetPm2:x,busy:o,online:tS,label:a("开始直播","Start"),className:"bg-emerald-500/20 text-emerald-200 ring-emerald-500/30",baseLock:!1,onRun:()=>p("start",x),notify:w,t:a}),(0,n.jsx)(eQ,{kind:"stop",targetPm2:x,busy:o,online:tS,label:a("停止直播","Stop"),className:"bg-rose-500/15 text-rose-200 ring-rose-500/25",baseLock:!1,onRun:()=>p("stop",x),notify:w,t:a}),(0,n.jsx)(eQ,{kind:"restart",targetPm2:x,busy:o,online:tS,label:a("重新开始","Restart"),className:"bg-amber-500/15 text-amber-200 ring-amber-500/25",baseLock:!1,onRun:()=>p("restart",x),notify:w,t:a})]}),(0,n.jsxs)("p",{className:"mt-3 text-center text-xs text-zinc-400",children:[a("当前状态","Current state"),": ",eq(eI,a)]}),(0,n.jsxs)("p",{className:"mt-3 text-center font-mono text-[11px] text-zinc-600",children:["key ",a("尾码","suffix"),": ",td||"—"]})]}):(0,n.jsx)(n.Fragment,{children:(0,n.jsxs)("div",{className:"mt-4 rounded-xl border border-cyan-500/25 bg-gradient-to-br from-cyan-500/[0.1] via-zinc-950/50 to-violet-500/[0.06] p-4 ring-1 ring-white/[0.04]",children:[(0,n.jsx)("h4",{className:"text-xs font-semibold uppercase tracking-wider text-cyan-200/90",children:a("Pro 转播实况","Pro relay status")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:a("对齐桌面端:每路独立 URL / youtube 配置文件;点左侧信息切换编辑,右侧控制进程。","Per-lane config files like desktop; click row info to edit, use right-side controls for PM2.")}),tw?(0,n.jsxs)("p",{className:"mt-2 text-[11px] text-zinc-500",children:[a("Neko 实例","Neko instances"),": ",tw]}):null,(0,n.jsx)("div",{className:"mt-3 max-h-[min(24rem,55vh)] space-y-2 overflow-y-auto pr-1",children:L.map(e=>{var t,s,l;let i=eM(e),r=eP(i),c=eT(i),d=eX((null!==(t=e.urlLines)&&void 0!==t?t:"").trim()),x=eh((null!==(s=e.urlLines)&&void 0!==s?s:"").trim()),u=x[0],m=em("key = ".concat(null!==(l=e.streamKey)&&void 0!==l?l:"")),h=T===e.id,p=eV(c),b=L.findIndex(t=>t.id===e.id),g=(0,eC.U_)(tf,b).link;return(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2.5 text-left text-xs transition sm:flex-nowrap ".concat(h?"border-violet-500/45 bg-violet-500/[0.1]":"border-white/10 bg-black/30 hover:border-white/18"),children:[(0,n.jsxs)("button",{type:"button",className:"min-w-0 flex-1 text-left",onClick:()=>{ey.current=eM(e),tn(e)},children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center gap-2",children:[(0,n.jsx)("span",{className:"font-medium text-zinc-100",children:e.name}),(0,n.jsx)("span",{className:"shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ring-1 ".concat(function(e){let t=((null==e?void 0:e.business_status)||"").trim().toLowerCase();return"ready"===t?"bg-sky-500/15 text-sky-100 ring-sky-500/25":"streaming"===t?"bg-emerald-500/15 text-emerald-200 ring-emerald-500/30":"waiting_source"===t||"monitoring"===t?"bg-amber-500/15 text-amber-100 ring-amber-500/25":"source_live"===t?"bg-sky-500/15 text-sky-100 ring-sky-500/25":"misconfigured"===t||"error"===t||"stale"===t?"bg-rose-500/15 text-rose-100 ring-rose-500/25":eV(null==e?void 0:e.process_status)?"bg-emerald-500/15 text-emerald-200 ring-emerald-500/30":"bg-zinc-600/20 text-zinc-400 ring-zinc-500/25"}(r)),children:eq(r,a)}),g?(0,n.jsx)("span",{className:"shrink-0 rounded-md bg-emerald-500/15 px-2 py-0.5 text-[10px] font-medium text-emerald-100 ring-1 ring-emerald-500/25",children:g.label}):null]}),(0,n.jsxs)("div",{className:"mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 font-mono text-[10px] text-zinc-500",children:[(0,n.jsxs)("span",{children:["key …",m||"—"]}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsx)("span",{className:"truncate",title:i,children:i}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsxs)("span",{children:["URL ",d.length]}),(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"}),(0,n.jsxs)("span",{children:["DY ",x.length]})]}),u?(0,n.jsx)("a",{href:u,target:"_blank",rel:"noreferrer",className:"mt-1 block max-w-full truncate text-cyan-300/90 underline-offset-2 hover:underline",onClick:e=>e.stopPropagation(),title:u,children:u.replace(/^https?:\/\//,"")}):(0,n.jsx)("span",{className:"mt-1 block text-zinc-600",children:"—"})]}),(0,n.jsxs)("div",{className:"flex w-full shrink-0 flex-wrap items-center justify-end gap-1.5 sm:w-auto",onClick:e=>e.stopPropagation(),onKeyDown:e=>e.stopPropagation(),children:[(0,n.jsx)(eQ,{kind:"start",targetPm2:i,busy:o,online:p,label:a("开始","Start"),className:"rounded-lg bg-emerald-500/20 text-emerald-100 ring-emerald-500/30",baseLock:!1,onRun:()=>void tk("start",i),notify:w,t:a,compact:!0}),(0,n.jsx)(eQ,{kind:"stop",targetPm2:i,busy:o,online:p,label:a("停止","Stop"),className:"rounded-lg bg-rose-500/15 text-rose-100 ring-rose-500/25",baseLock:!1,onRun:()=>void tk("stop",i),notify:w,t:a,compact:!0}),(0,n.jsx)(eQ,{kind:"restart",targetPm2:i,busy:o,online:p,label:a("重新开始","Restart"),className:"rounded-lg bg-amber-500/15 text-amber-100 ring-amber-500/25",baseLock:!1,onRun:()=>void tk("restart",i),notify:w,t:a,compact:!0})]})]},e.id)})})]})})]}),"single"===R?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("YouTube 相关设置","YouTube settings")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("串流参数写进 youtube.ini,无需 Google 登录。","Stream settings go to youtube.ini — no Google login.")}),(0,n.jsxs)("div",{className:"mt-4 space-y-3",children:[(0,n.jsx)("label",{className:"block text-xs text-zinc-400",children:a("串流密钥","Stream key")}),(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-4 py-3 text-sm font-mono text-zinc-200",spellCheck:!1,autoComplete:"off",value:A.key,onChange:e=>D(t=>({...t,key:e.target.value})),placeholder:"xxxx-xxxx-xxxx-xxxx-xxxx"}),(0,n.jsxs)("div",{className:"grid gap-3 sm:grid-cols-3",children:[(0,n.jsxs)("label",{className:"flex flex-col gap-1 text-xs text-zinc-400",children:["RTMPS",(0,n.jsxs)("select",{className:"rounded-lg border border-white/10 bg-black/40 px-3 py-2 text-sm text-white",value:A.rtmps?"1":"0",onChange:e=>D(t=>({...t,rtmps:"1"===e.target.value})),children:[(0,n.jsx)("option",{value:"1",children:a("是(默认)","Yes")}),(0,n.jsx)("option",{value:"0",children:a("否 → RTMP","No")})]})]}),(0,n.jsxs)("label",{className:"flex flex-col gap-1 text-xs text-zinc-400",children:[a("比特率 kbps","Bitrate kbps"),(0,n.jsx)("input",{className:"rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-sm",value:A.bitrate,onChange:e=>D(t=>({...t,bitrate:e.target.value})),placeholder:"4000"})]}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center gap-2 pt-6 text-xs text-zinc-300",children:[(0,n.jsx)("input",{type:"checkbox",className:"rounded border-white/20 bg-black/40",checked:A.fastAudio,onChange:e=>D(t=>({...t,fastAudio:e.target.checked}))}),"fast_audio(",a("轻量音频","lighter audio"),")"]})]})]}),(0,n.jsxs)("div",{className:"mt-4 flex flex-wrap gap-2",children:[(0,n.jsxs)("button",{type:"button",disabled:tz,onClick:()=>void tl(),className:"inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-200",children:[(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5 ".concat(H?"animate-spin":"")}),a("重新读取","Reload")]}),(0,n.jsxs)("button",{type:"button",disabled:tz,onClick:()=>void to(),className:"inline-flex items-center gap-2 rounded-xl bg-violet-500/25 px-4 py-2 text-sm font-medium text-violet-100 ring-1 ring-violet-500/35",children:[H?(0,n.jsx)(v.Z,{className:"h-4 w-4 animate-spin"}):null,a("保存 youtube.ini","Save youtube.ini")]}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void tc(),className:"rounded-xl border border-cyan-500/20 bg-cyan-500/10 px-4 py-2 text-sm text-cyan-100",children:a("平衡预设","Balanced preset")}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>{D(ep(E)),O(a("已从下方原文同步到表单","Synced from raw"))},className:"rounded-xl border border-white/10 px-4 py-2 text-sm text-zinc-400",children:a("从原文同步表单","Parse raw")})]}),(0,n.jsxs)("details",{className:"mt-4 rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("summary",{className:"cursor-pointer text-xs text-zinc-500",children:a("高级:youtube.ini 原文","Raw youtube.ini")}),(0,n.jsx)("textarea",{className:"mt-2 h-36 w-full resize-y rounded-lg border border-white/10 bg-black/50 p-3 font-mono text-[11px] text-zinc-400",value:E,onChange:e=>I(e.target.value),spellCheck:!1}),(0,n.jsx)("button",{type:"button",disabled:tz,className:"mt-2 rounded-lg bg-cyan-500/15 px-3 py-1.5 text-xs text-cyan-200",onClick:()=>void ta(E),children:a("保存原文","Save raw")})]}),Z?(0,n.jsx)("p",{className:"mt-2 font-mono text-xs text-zinc-500",children:Z}):null]}):(0,n.jsxs)("div",{className:"space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsxs)("h3",{className:"text-sm font-semibold text-white",children:[a("当前线路","Current lane"),e3?(0,n.jsxs)("span",{className:"ml-2 font-normal text-zinc-400",children:["\xb7 ",e3.name]}):null,tt?(0,n.jsx)("span",{className:"ml-2 rounded-md bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-100 ring-1 ring-amber-500/25",children:a("有未保存修改","Unsaved")}):null]}),e3?(0,n.jsxs)("p",{className:"mt-0.5 font-mono text-[11px] text-zinc-500",children:["PM2: ",e8,e7?" → ".concat(e9):""," \xb7 ","key ",td||"—"]}):null]}),(0,n.jsxs)("button",{type:"button",disabled:tz,className:"inline-flex items-center gap-1 rounded-lg bg-violet-500/20 px-3 py-1.5 text-xs text-violet-100 ring-1 ring-violet-500/30",onClick:()=>{let e=eZ(),t={id:e,name:"".concat(a("线路","Ch")," ").concat(L.length+1),streamKey:"",urlLines:"",pm2Name:eO(e),notes:""};et(s=>({...s,[e]:eG(t)})),e2([...L,t]),U(e),ey.current=eM(t),u(ey.current),ek("")},children:[(0,n.jsx)(e_.Z,{className:"h-3.5 w-3.5"}),a("新增线路","Add lane")]})]}),e3?(0,n.jsxs)("div",{className:"mt-4 space-y-3 border-t border-white/5 pt-4",children:[(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm",value:null!==(s=null==e4?void 0:e4.name)&&void 0!==s?s:"",onChange:e=>e6("name",e.target.value),placeholder:a("线路名称","Lane name")}),(0,n.jsx)("label",{className:"block text-xs text-zinc-500",children:a("串流密钥","Stream key")}),(0,n.jsx)("input",{className:"w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 font-mono text-sm",spellCheck:!1,value:null!==(l=null==e4?void 0:e4.streamKey)&&void 0!==l?l:"",onChange:e=>e6("streamKey",e.target.value),placeholder:"xxxx-xxxx… / rtmp(s)://…"}),(0,n.jsxs)("div",{className:"flex flex-wrap gap-2",children:[(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void tr(),className:"rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-100",children:es?a("保存中...","Saving..."):a("保存线路设置","Save lane")}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void th(),className:"rounded-xl bg-violet-500/25 px-4 py-2 text-sm font-medium text-violet-100 ring-1 ring-violet-500/35",children:a("写入 youtube.ini","Write youtube.ini")})]}),te.ok?null:(0,n.jsx)("p",{className:"text-xs text-rose-300",children:te.message}),(0,n.jsxs)("details",{className:"rounded-xl border border-white/5 bg-black/20 p-3",children:[(0,n.jsx)("summary",{className:"cursor-pointer text-xs text-zinc-500",children:a("高级:进程名","Advanced: process name")}),(0,n.jsx)("input",{className:"mt-2 w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-xs",value:null!==(i=null==e4?void 0:e4.pm2Name)&&void 0!==i?i:eO(e3.id),onChange:e=>e6("pm2Name",e.target.value),spellCheck:!1,placeholder:"youtube2__…"}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-[11px] text-zinc-500",children:[a("实际生效","Effective"),": ",e9||"—"]}),te.message&&!te.ok?(0,n.jsx)("p",{className:"mt-2 text-[11px] text-rose-300",children:te.message}):null]}),(0,n.jsx)("textarea",{className:"h-16 w-full resize-y rounded-xl border border-white/10 bg-black/40 p-3 text-xs text-zinc-300",value:null!==(r=null==e4?void 0:e4.notes)&&void 0!==r?r:"",onChange:e=>e6("notes",e.target.value),placeholder:a("备注(可选)","Notes (optional)")}),el?(0,n.jsx)("p",{className:"text-xs text-zinc-400",children:el}):null,(0,n.jsxs)("button",{type:"button",disabled:tz||L.length<2,className:"inline-flex items-center gap-1 text-xs text-rose-300 hover:text-rose-200 disabled:opacity-30",onClick:()=>{var e;let t=L.filter(e=>e.id!==e3.id);et(e=>{let t={...e};return delete t[e3.id],t}),e2(t),U((null===(e=t[0])||void 0===e?void 0:e.id)||"")},children:[(0,n.jsx)(ed.Z,{className:"h-3.5 w-3.5"}),a("删除此线路","Delete lane")]})]}):null]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播地址列表","URL list")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("每条线路独立写入自己的 URL_config..ini,不再复用或回填所谓“全局地址”。","Each lane writes to its own URL_config..ini with no shared global URL draft.")}),(0,n.jsx)("p",{className:"mt-1 font-mono text-[10px] text-zinc-600",children:x?eb(x):"URL_config.ini"}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2 text-[11px]",children:[(0,n.jsxs)("span",{className:"rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100",children:[a("有效地址","Valid URLs"),": ",tb]}),(0,n.jsxs)("span",{className:"rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100",children:["Douyin: ",tg]})]}),t_,$?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:$}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[160px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:F,onChange:e=>{eo.current=!0,ek(e.target.value)},spellCheck:!1,placeholder:"https://live.douyin.com/..."}),(0,n.jsx)("div",{className:"mt-3 flex flex-wrap gap-2",children:(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void tx(),className:"rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:a("保存地址","Save URLs")})})]})]}),"single"===R?(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-6",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:a("直播地址列表 \xb7 URL_config.ini","URL_config.ini")}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500",children:a("一行一个地址;可自动刷新或手动重新读取。","One URL per line; auto-refresh or reload.")}),(0,n.jsxs)("div",{className:"mt-3 flex flex-wrap gap-2 text-[11px]",children:[(0,n.jsxs)("span",{className:"rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100",children:[a("有效地址","Valid URLs"),": ",tb]}),(0,n.jsxs)("span",{className:"rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100",children:["Douyin: ",tg]})]}),t_,$?(0,n.jsx)("p",{className:"mt-2 text-xs text-zinc-500",children:$}):null,(0,n.jsx)("textarea",{className:"mt-4 min-h-[200px] w-full resize-y rounded-xl border border-white/10 bg-black/50 p-4 font-mono text-xs text-zinc-300",value:m,onChange:e=>{eo.current=!0,ej(e.target.value)},spellCheck:!1}),(0,n.jsx)("button",{type:"button",disabled:tz,onClick:()=>void tu(),className:"mt-3 rounded-xl bg-cyan-500/20 px-4 py-2 text-sm text-cyan-100 ring-1 ring-cyan-500/30",children:a("保存 URL 配置","Save URL config")})]}):null,(0,n.jsx)(ev,{t:a,currentProc:x,fetchJson:g,urlListText:"single"===R?m:F,keySuffixUi:td,showKeyRow:!0,pollMs:800,refreshToken:er})]})}var e1=s(257);let e5=()=>void 0!==e1&&e1.env.NEXT_PUBLIC_API_URL||"";function e2(e){if(null==e||!Number.isFinite(e))return"—";if(0===e)return"0 B";let t=["B","KB","MB","GB","TB"],s=e,n=0;for(;s>=1024&&n=10||0===n?0:1)," ").concat(t[n])}function e3(e){return"online"===e||"running"===e?"bg-emerald-500/15 text-emerald-300 ring-1 ring-emerald-500/30":"configured"===e?"bg-sky-500/15 text-sky-200 ring-1 ring-sky-500/25":"skipped"===e?"bg-slate-500/15 text-slate-300 ring-1 ring-slate-500/30":"stopped"===e||"not_found"===e?"bg-zinc-500/15 text-zinc-400 ring-1 ring-zinc-500/25":"error"===e?"bg-rose-500/15 text-rose-300 ring-1 ring-rose-500/35":"bg-violet-500/15 text-violet-200 ring-1 ring-violet-500/30"}function e4(e,t){let s=(e||"").trim().toLowerCase();return"online"===s||"running"===s?t("运行中","Running"):"configured"===s?t("已配置","Configured"):"not_found"===s?t("未注册","Unregistered"):"stopped"===s?t("已停止","Stopped"):"error"===s?t("异常","Error"):e||"—"}function e6(){var e,t,s,L,M,T,U,E,I,A,D,Z,O,H,B,K,F,W,J,G,V,q,X,$,Q,ee,et,es,en,el,ei,ea,eo,ec,ed,eu,em,eh,ep,eb,eg,ef,ev,ey,ew,eN,ej,ek,eS,e_,eR,eP,eL,eM,eT,eU,eE,eI,eA;let eD=e5(),[eZ,eO]=(0,_.useState)("zh"),[eH,eB]=(0,_.useState)("dark"),[eK,eF]=(0,_.useState)("dashboard"),[eW,eY]=(0,_.useState)(!1),[eJ,eG]=(0,_.useState)(null),[eV,eq]=(0,_.useState)(null),[eX,e$]=(0,_.useState)(!0),[eQ,e1]=(0,_.useState)(null),[e6,e8]=(0,_.useState)(null),[e9,e7]=(0,_.useState)(null),[te,tt]=(0,_.useState)(!0),[ts,tn]=(0,_.useState)([]),[tl,ti]=(0,_.useState)([]),[tr,ta]=(0,_.useState)(""),[to,tc]=(0,_.useState)(""),[td,tx]=(0,_.useState)("/sdcard/app.apk"),[tu,tm]=(0,_.useState)(""),[th,tp]=(0,_.useState)(null),[tb,tg]=(0,_.useState)(null),[tf,tv]=(0,_.useState)(!0),[ty,tw]=(0,_.useState)(!0),tN=(0,_.useRef)(null),tj=(0,_.useRef)(null),tk=(0,_.useRef)(tr),tz=(0,_.useRef)(0),tS=(0,_.useRef)(null),t_=(0,_.useCallback)((e,t)=>"zh"===eZ?e:t,[eZ]),tC=(0,_.useCallback)(e=>{e8(e),null!=tS.current&&window.clearTimeout(tS.current),tS.current=window.setTimeout(()=>{e8(null),tS.current=null},2800)},[]),tR=(0,_.useCallback)(async(e,t)=>(0,Y.N)("".concat(eD).concat(e),t),[eD]),tP=(0,_.useCallback)(async()=>{if(tN.current)return tN.current;let e=(async()=>{try{var e;let[t,s]=await Promise.allSettled([tR("/hub/dashboard"),tR("/deploy/check")]);if("fulfilled"!==t.status)throw t.reason;let n=t.value;eG(n),eq(null),e7(Date.now()),"fulfilled"===s.status?tg(s.value):tg(null);let l=(null===(e=n.android)||void 0===e?void 0:e.devices)||[];if(l.length&&!tu){let e=l.find(e=>"device"===(e.state||"").toLowerCase());tm((null!=e?e:l[0]).serial)}}catch(e){eq(e.message),tg(null)}finally{e$(!1),tN.current=null}})();return tN.current=e,e},[tu,tR]),tL=(0,_.useMemo)(()=>{let e=new URLSearchParams({light:"1"});return"live_youtube"===eK?e.set("family","youtube"):"live_tiktok"===eK&&e.set("family","tiktok"),"/process_monitor?".concat(e.toString())},[eK]),tM=(0,_.useCallback)(async()=>{if(tj.current)return tj.current;let e=(async()=>{try{let e=(await tR(tL)).entries||[];ti(e),tn(e.map(e=>({pm2:e.pm2,script:e.script,label:e.label}))),e7(Date.now()),ta(t=>!e.length||t&&e.some(e=>e.pm2===t)?t:e[0].pm2)}catch(e){}finally{tj.current=null}})();return tj.current=e,e},[tR,tL]);(0,_.useEffect)(()=>{let e=window.localStorage.getItem("live-hub-lang");("zh"===e||"en"===e)&&eO(e);let t=window.localStorage.getItem("live-hub-theme");("light"===t||"dark"===t)&&eB(t)},[]),(0,_.useEffect)(()=>{window.localStorage.setItem("live-hub-lang",eZ)},[eZ]),(0,_.useEffect)(()=>{document.documentElement.setAttribute("data-theme",eH);try{window.localStorage.setItem("live-hub-theme",eH)}catch(e){}},[eH]),(0,_.useEffect)(()=>{let e=()=>tt("visible"===document.visibilityState);return e(),document.addEventListener("visibilitychange",e),()=>document.removeEventListener("visibilitychange",e)},[]),(0,_.useEffect)(()=>{if(!eW)return;let e=e=>{"Escape"===e.key&&eY(!1)};window.addEventListener("keydown",e);let t=document.body.style.overflow;return document.body.style.overflow="hidden",()=>{window.removeEventListener("keydown",e),document.body.style.overflow=t}},[eW]),(0,_.useEffect)(()=>()=>{null!=tS.current&&window.clearTimeout(tS.current)},[]),(0,_.useEffect)(()=>{try{"0"===localStorage.getItem("livehub.autorefresh")&&tv(!1),"0"===localStorage.getItem("livehub.showStreamProbes")&&tw(!1)}catch(e){}},[]),(0,_.useEffect)(()=>{tP()},[tP]),(0,_.useEffect)(()=>{tM()},[tM]),(0,_.useEffect)(()=>{if(!tf||!te||"live_youtube"===eK||"live_tiktok"===eK)return;let e=window.setInterval(()=>{tP()},1e4);return()=>clearInterval(e)},[tf,te,tP,eK]),(0,_.useEffect)(()=>{if(!tf||!te)return;let e=window.setInterval(()=>{tM()},"live_youtube"===eK||"live_tiktok"===eK?700:4e3);return()=>clearInterval(e)},[tf,te,tM,eK]),(0,_.useEffect)(()=>{tk.current=tr},[tr]),(0,_.useEffect)(()=>{tr&&"live_youtube"===eK&&(async()=>{let e=tk.current.trim(),t=++tz.current;try{let s=new URLSearchParams({process:e,_ts:String(Date.now())}),n=await tR("/get_url_config?".concat(s));if(t!==tz.current||e!==tk.current.trim())return;tc(n.content||"")}catch(e){}})()},[tr,tR,eK]),(0,_.useEffect)(()=>{"settings"===eK&&(async()=>{try{let e=await tR("/hub/config");tp(e)}catch(e){tp(null)}})()},[tR,eK]);let tT=(0,_.useMemo)(()=>{var e;let t=(null==eJ?void 0:null===(e=eJ.stack)||void 0===e?void 0:e.quick_links)||{},s={};for(let[e,n]of Object.entries(t))/^https?:\/\//i.test(n)?s[e]=(0,ex.p)(n):s[e]=n;return s},[null==eJ?void 0:null===(e=eJ.stack)||void 0===e?void 0:e.quick_links]),tU=(0,_.useMemo)(()=>(0,eC.O0)(tT),[tT]),tE=(0,_.useMemo)(()=>(0,eC.Tc)(tU),[tU]),tI=async(e,t)=>{e1("svc:".concat(e));try{let s=await tR("/service_action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({service:e,action:t})});"error"===s.status?tC(s.detail||s.message||t_("操作失败","Action failed")):tC(s.message||"OK"),await tP()}catch(e){tC(e.message)}finally{e1(null)}},tA=async(e,t)=>{let s=(null!=t?t:tr).trim();if(s){e1("proc:".concat(e,":").concat(s));try{let t=await tR("/".concat(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({process:s})});tC(String(t.output||t.message||"OK")),await tM(),"live_youtube"===eK||"live_tiktok"===eK?tP():await tP()}catch(e){tC(e.message)}finally{e1(null)}}},tD=async(e,t)=>{let s=(null!=t?t:tr).trim();if(!s)return;let n=null!=e?e:to,l=++tz.current;e1("save:url");try{let e=new URLSearchParams({process:s});await tR("/save_url_config?".concat(e),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({content:n})}),l===tz.current&&s===tk.current.trim()&&tc(n),tC(t_("URL 配置已保存","URL config saved"))}catch(e){throw tC(e.message),e}finally{e1(null)}},tZ=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!tu){tC(t_("请选择设备","Pick a device"));return}e1("adb:".concat(e));try{let s=await tR("/android/action",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({action:e,serial:tu,payload:t})});tC(s.message||"OK")}catch(e){tC(e.message)}finally{e1(null)}},tO=[{id:"dashboard",icon:r.Z,labelZh:"总览",labelEn:"Dashboard"},{id:"services",icon:a.Z,labelZh:"服务",labelEn:"Services"},{id:"live_youtube",icon:o.Z,labelZh:"YouTube 无人直播",labelEn:"YouTube unmanned"},{id:"live_tiktok",icon:c.Z,labelZh:"TikTok 无人直播",labelEn:"TikTok unmanned"},{id:"android",icon:d.Z,labelZh:"安卓",labelEn:"Android"},{id:"network",icon:x.Z,labelZh:"网络",labelEn:"Network"},{id:"settings",icon:u.Z,labelZh:"设置",labelEn:"Settings"}],tH=e=>{(async()=>{var t;if(null===(t=navigator.clipboard)||void 0===t?void 0:t.writeText){await navigator.clipboard.writeText(e);return}let s=document.createElement("textarea");s.value=e,s.readOnly=!0,s.style.position="fixed",s.style.opacity="0",document.body.appendChild(s),s.select(),document.execCommand("copy"),s.remove()})().then(()=>tC(t_("已复制","Copied")),()=>tC(t_("复制失败","Copy failed")))},tB=null!==(ev=null==eJ?void 0:null===(t=eJ.services)||void 0===t?void 0:t.filter(e=>e.running&&e.available).length)&&void 0!==ev?ev:0,tK=null!==(ey=null==eJ?void 0:null===(s=eJ.services)||void 0===s?void 0:s.length)&&void 0!==ey?ey:0,tF=e9?new Date(e9).toLocaleTimeString("zh"===eZ?"zh-CN":"en-US",{hour12:!1}):t_("未同步","Not synced"),tW=te?t_("前台自动刷新","Auto refresh"):t_("后台已暂停刷新","Refresh paused in background");return(0,n.jsxs)("div",{className:"lp-root min-h-screen transition-colors duration-300 ".concat("light"===eH?"bg-slate-100 text-slate-900 selection:bg-violet-400/25":"bg-[#050508] text-zinc-100 selection:bg-violet-500/30"),children:[(0,n.jsx)("div",{className:"lp-noise pointer-events-none fixed inset-0 z-0 ".concat("light"===eH?"opacity-[0.06]":"opacity-[0.22]"),"aria-hidden":!0}),(0,n.jsx)("div",{className:"lp-aurora pointer-events-none fixed -left-1/4 top-0 h-[42rem] w-[42rem] rounded-full blur-[120px] ".concat("light"===eH?"bg-violet-400/15":"bg-violet-600/20"),"aria-hidden":!0}),(0,n.jsx)("div",{className:"lp-aurora2 pointer-events-none fixed bottom-0 right-0 h-[36rem] w-[36rem] rounded-full blur-[100px] ".concat("light"===eH?"bg-cyan-400/10":"bg-cyan-500/10"),"aria-hidden":!0}),e6?(0,n.jsx)(l.E.div,{initial:{opacity:0,y:16},animate:{opacity:1,y:0},role:"status","aria-live":"polite","aria-atomic":"true",className:"fixed bottom-6 left-1/2 z-[100] max-w-md -translate-x-1/2 rounded-2xl border border-white/10 bg-zinc-950/95 px-5 py-3 text-center text-sm text-zinc-100 shadow-2xl shadow-black/50 backdrop-blur-xl",children:e6}):null,(0,n.jsxs)("div",{className:"relative z-10 flex min-h-screen",children:[(0,n.jsxs)("aside",{className:"hidden w-64 shrink-0 flex-col border-r px-4 py-6 backdrop-blur-2xl lg:flex ".concat("light"===eH?"border-slate-200/90 bg-white/70":"border-white/[0.06] bg-zinc-950/40"),children:[(0,n.jsxs)("div",{className:"flex items-center gap-2 px-2",children:[(0,n.jsx)("div",{className:"flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg shadow-violet-500/25",children:(0,n.jsx)(m.Z,{className:"h-4 w-4 text-white"})}),(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-[0.2em] ".concat("light"===eH?"text-slate-500":"text-zinc-500"),children:"live.local"}),(0,n.jsx)("p",{className:"text-sm font-semibold ".concat("light"===eH?"text-slate-900":"text-white"),children:t_("无人直播系统","Unmanned Live System")})]})]}),(0,n.jsx)("nav",{className:"mt-10 flex flex-1 flex-col gap-1",children:tO.map(e=>{let t=e.icon,s=eK===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>eF(e.id),"aria-current":s?"page":void 0,className:"flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-medium transition ".concat("light"===eH?s?"bg-violet-100/90 text-slate-900 shadow-inner shadow-violet-200/50":"text-slate-600 hover:bg-slate-200/60 hover:text-slate-900":s?"bg-white/[0.08] text-white shadow-inner shadow-white/5":"text-zinc-500 hover:bg-white/[0.04] hover:text-zinc-200"),children:[(0,n.jsx)(t,{className:"h-4 w-4 ".concat("light"===eH?s?"text-violet-600":"text-slate-400":s?"text-violet-300":"text-zinc-600")}),t_(e.labelZh,e.labelEn),s?(0,n.jsx)(h.Z,{className:"ml-auto h-4 w-4 ".concat("light"===eH?"text-slate-400":"text-zinc-600")}):null]},e.id)})})]}),(0,n.jsxs)("div",{className:"flex min-w-0 flex-1 flex-col",children:[(0,n.jsxs)("header",{className:"sticky top-0 z-20 flex items-center justify-between gap-3 border-b px-4 py-3 backdrop-blur-xl lg:px-8 ".concat("light"===eH?"border-slate-200/80 bg-white/75 text-slate-900":"border-white/[0.06] bg-zinc-950/70 text-white"),children:[(0,n.jsxs)("div",{className:"flex min-w-0 items-center gap-3",children:[(0,n.jsx)("button",{type:"button",className:"rounded-xl border p-2 lg:hidden ".concat("light"===eH?"border-slate-200 bg-white/80":"border-white/10 bg-white/5"),onClick:()=>eY(!0),"aria-label":"Menu",children:(0,n.jsx)(p.Z,{className:"h-5 w-5"})}),(0,n.jsxs)("div",{className:"min-w-0",children:[(0,n.jsx)("h1",{className:"truncate text-lg font-semibold tracking-tight lg:text-xl ".concat("light"===eH?"text-slate-900":"text-white"),children:t_((null===(L=tO.find(e=>e.id===eK))||void 0===L?void 0:L.labelZh)||"",(null===(M=tO.find(e=>e.id===eK))||void 0===M?void 0:M.labelEn)||"")}),(0,n.jsxs)("p",{className:"truncate text-xs ".concat("light"===eH?"text-slate-500":"text-zinc-500"),children:[(null==eJ?void 0:null===(T=eJ.stack)||void 0===T?void 0:T.mdns_host)||"live.local"," \xb7"," ","dark"===eH?t_("夜间","Dark mode"):t_("日间","Light mode")," \xb7 ",tW," \xb7"," ",t_("最后同步","Last sync")," ",tF]})]})]}),(0,n.jsxs)("div",{className:"flex shrink-0 items-center gap-1.5 sm:gap-2",children:[(0,n.jsx)("button",{type:"button",onClick:()=>eB(e=>"dark"===e?"light":"dark"),className:"rounded-xl border p-2 transition ".concat("light"===eH?"border-slate-200 bg-white text-amber-500 shadow-sm":"border-white/10 bg-white/[0.06] text-violet-200"),title:"dark"===eH?t_("切换到日间","Switch to light"):t_("切换到夜间","Switch to dark"),"aria-label":"dark"===eH?t_("日间模式","Light mode"):t_("夜间模式","Dark mode"),"aria-pressed":"light"===eH,children:"dark"===eH?(0,n.jsx)(b.Z,{className:"h-5 w-5",strokeWidth:2}):(0,n.jsx)(g.Z,{className:"h-5 w-5",strokeWidth:2})}),(0,n.jsxs)("button",{type:"button",onClick:()=>eO(e=>"zh"===e?"en":"zh"),className:"relative rounded-xl border p-2 transition ".concat("light"===eH?"border-slate-200 bg-white text-cyan-600 shadow-sm":"border-white/10 bg-white/[0.06] text-cyan-200"),title:"zh"===eZ?"English":"中文","aria-label":t_("切换语言","Toggle language"),"aria-pressed":"en"===eZ,children:[(0,n.jsx)(f.Z,{className:"h-5 w-5",strokeWidth:2}),(0,n.jsx)("span",{className:"absolute -bottom-0.5 -right-0.5 flex h-4 min-w-[1rem] items-center justify-center rounded px-0.5 text-[9px] font-bold leading-none ".concat("light"===eH?"bg-cyan-100 text-cyan-800":"bg-cyan-500/30 text-cyan-100"),children:"zh"===eZ?"中":"A"})]}),(0,n.jsxs)("button",{type:"button",disabled:!!eQ,onClick:()=>void Promise.all([tP(),tM()]),className:"flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-medium transition disabled:opacity-40 ".concat("light"===eH?"border-slate-200 bg-white text-slate-700 hover:bg-slate-50":"border-white/10 bg-white/[0.04] text-zinc-300 hover:bg-white/[0.07]"),children:[eQ?(0,n.jsx)(v.Z,{className:"h-3.5 w-3.5 animate-spin"}):(0,n.jsx)(y.Z,{className:"h-3.5 w-3.5"}),(0,n.jsx)("span",{className:"hidden sm:inline",children:t_("刷新","Refresh")})]})]})]}),(0,n.jsx)("main",{className:"flex-1 px-4 py-6 lg:px-8 lg:py-8",children:(0,n.jsx)(i.M,{mode:"wait",children:(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},exit:{opacity:0,y:-6},transition:{duration:.28,ease:[.22,1,.36,1]},children:["dashboard"===eK?(0,n.jsxs)("div",{className:"space-y-6",children:[eV?(0,n.jsxs)("div",{className:"rounded-2xl border border-rose-500/25 bg-rose-950/20 p-4 text-sm text-rose-200",children:[t_("无法加载仪表盘:","Dashboard error: "),eV]}):null,(0,n.jsx)(l.E.div,{initial:{opacity:0,y:8},animate:{opacity:1,y:0},transition:{delay:.02},className:"rounded-2xl border p-2 backdrop-blur-xl ".concat("light"===eH?"border-slate-200/90 bg-white/80 shadow-sm shadow-slate-200/30":"border-white/[0.07] bg-zinc-950/30"),children:(0,n.jsx)(R.Z,{snapshot:null!==(ew=null==eJ?void 0:eJ.snapshot)&&void 0!==ew?ew:null,theme:eH,tr:t_})}),(0,n.jsx)(l.E.div,{initial:{opacity:0,y:8},animate:{opacity:1,y:0},transition:{delay:.05},className:"rounded-2xl border p-3 shadow-lg ".concat("light"===eH?"border-fuchsia-200/80 bg-gradient-to-br from-fuchsia-50/90 via-white to-cyan-50/70 shadow-fuchsia-100/40":"border-fuchsia-500/20 bg-gradient-to-br from-fuchsia-500/10 via-zinc-950/60 to-cyan-500/10 shadow-fuchsia-900/10"),children:(0,n.jsx)(C,{tr:t_})}),(0,n.jsx)("div",{className:"grid gap-2 sm:grid-cols-2 xl:grid-cols-5",children:[{icon:w.Z,label:t_("CPU 1m","CPU 1m"),value:null!==(eN=null==eJ?void 0:null===(I=eJ.snapshot)||void 0===I?void 0:null===(E=I.cpu_load)||void 0===E?void 0:null===(U=E["1m"])||void 0===U?void 0:U.toFixed(2))&&void 0!==eN?eN:"—",sub:t_("负载均值","Load avg")},{icon:r.Z,label:t_("内存","Memory"),value:e2(null==eJ?void 0:null===(D=eJ.snapshot)||void 0===D?void 0:null===(A=D.memory)||void 0===A?void 0:A.available),sub:"/ ".concat(e2(null==eJ?void 0:null===(O=eJ.snapshot)||void 0===O?void 0:null===(Z=O.memory)||void 0===Z?void 0:Z.total))},{icon:N.Z,label:t_("磁盘可用","Disk free"),value:e2(null==eJ?void 0:null===(B=eJ.snapshot)||void 0===B?void 0:null===(H=B.disk)||void 0===H?void 0:H.free),sub:"/ ".concat(e2(null==eJ?void 0:null===(F=eJ.snapshot)||void 0===F?void 0:null===(K=F.disk)||void 0===K?void 0:K.total))},{icon:a.Z,label:t_("服务在线","Services up"),value:"".concat(tB,"/").concat(tK||"—"),sub:t_("基础服务层","Infra layer")},{icon:j.Z,label:t_("运行时间","Uptime"),value:function(e){if(null==e||!Number.isFinite(e))return"—";let t=Math.max(0,Math.floor(e)),s=Math.floor(t/86400),n=Math.floor(t%86400/3600),l=Math.floor(t%3600/60);return s>0?"".concat(s,"d ").concat(n,"h"):n>0?"".concat(n,"h ").concat(l,"m"):"".concat(l,"m")}(null==eJ?void 0:null===(W=eJ.snapshot)||void 0===W?void 0:W.uptime_seconds),sub:t_("自开机","Since boot")}].map((e,t)=>(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:12},animate:{opacity:1,y:0},transition:{delay:.05*t,duration:.35,ease:[.22,1,.36,1]},whileHover:{y:-2},className:"group relative overflow-hidden rounded-xl border border-white/[0.07] bg-gradient-to-br from-white/[0.05] to-transparent p-3 shadow-lg shadow-black/20",children:[(0,n.jsx)("div",{className:"absolute -right-4 -top-4 h-16 w-16 rounded-full bg-violet-500/10 blur-xl transition group-hover:bg-violet-500/18"}),(0,n.jsx)(e.icon,{className:"h-4 w-4 text-violet-300/90"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:e.label}),(0,n.jsx)("p",{className:"mt-0.5 font-mono text-lg font-semibold leading-tight text-white",children:eX?"…":e.value}),(0,n.jsx)("p",{className:"mt-0.5 text-[10px] text-zinc-500",children:e.sub})]},e.label))}),(null==eJ?void 0:eJ.hardware)&&"object"==typeof eJ.hardware?(0,n.jsxs)("div",{className:"rounded-2xl border border-indigo-500/25 bg-gradient-to-br from-indigo-500/[0.1] via-zinc-950/70 to-violet-500/[0.08] p-6 shadow-2xl shadow-indigo-500/5",children:[(0,n.jsx)("h3",{className:"text-base font-semibold text-white",children:t_("系统硬件与软件能力","Hardware & software profile")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:t_("来自 hardware_probe;用于直播/解码策略参考。","From hardware_probe; guides decode/streaming posture.")}),(0,n.jsxs)("div",{className:"mt-5 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-indigo-300/90",children:t_("平台","Platform")}),(0,n.jsxs)("dl",{className:"space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"arch"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(ej=eJ.hardware.arch)&&void 0!==ej?ej:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"kernel"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(eS=null!==(ek=eJ.hardware.kernel)&&void 0!==ek?ek:null===(J=eJ.stack)||void 0===J?void 0:J.kernel)&&void 0!==eS?eS:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"Python"}),(0,n.jsx)("dd",{className:"text-right text-zinc-200",children:String(null!==(e_=null===(G=eJ.stack)||void 0===G?void 0:G.python)&&void 0!==e_?e_:"—")})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"mDNS"}),(0,n.jsx)("dd",{className:"text-right text-cyan-200/90",children:null!==(eR=null===(V=eJ.stack)||void 0===V?void 0:V.mdns_host)&&void 0!==eR?eR:"—"})]}),(0,n.jsxs)("div",{className:"flex justify-between gap-2",children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"IP"}),(0,n.jsx)("dd",{className:"text-right text-zinc-300",children:((null===(q=eJ.stack)||void 0===q?void 0:q.ips)||[]).join(" \xb7 ")||"—"})]})]})]}),(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-fuchsia-300/90",children:"GPU / NPU"}),(()=>{let e=eJ.hardware.gpu,t=eJ.hardware.npu;return(0,n.jsxs)("dl",{className:"space-y-2 font-mono text-[11px] text-zinc-400",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"GPU"}),(0,n.jsxs)("dd",{className:"mt-0.5 text-zinc-200",children:[(null==e?void 0:e.present)?t_("已检测","present"):t_("未检测","absent"),(null==e?void 0:e.driver)?" \xb7 ".concat(e.driver):""]})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"render"}),(0,n.jsx)("dd",{className:"mt-0.5 break-all text-zinc-500",children:((null==e?void 0:e.render_nodes)||[]).join(", ")||"—"})]}),(0,n.jsxs)("div",{children:[(0,n.jsx)("dt",{className:"text-zinc-600",children:"NPU"}),(0,n.jsx)("dd",{className:"mt-0.5 break-all text-zinc-500",children:(null==t?void 0:t.present)?(t.devices||[]).join(", "):t_("—","—")})]})]})})()]}),(0,n.jsxs)("div",{className:"space-y-3 rounded-xl border border-white/[0.06] bg-black/30 p-4 lg:col-span-2",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-emerald-300/90",children:"ffmpeg / 建议策略"}),(()=>{var e,t,s,l;let i=eJ.hardware.ffmpeg_hwaccels||[],r=eJ.hardware.recommendation;return(0,n.jsxs)("div",{className:"space-y-2 text-[11px] text-zinc-400",children:[(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"hwaccels: "}),(0,n.jsx)("span",{className:"font-mono text-emerald-200/90",children:i.length?i.join(", "):"—"})]}),(0,n.jsxs)("div",{className:"grid gap-2 sm:grid-cols-2",children:[(0,n.jsxs)("p",{children:[(0,n.jsxs)("span",{className:"text-zinc-600",children:[t_("解码","Decoder"),": "]}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(e=null==r?void 0:r.video_decoder)&&void 0!==e?e:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"HDMI: "}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(t=null==r?void 0:r.hdmi_player)&&void 0!==t?t:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsx)("span",{className:"text-zinc-600",children:"Browser: "}),(0,n.jsx)("span",{className:"text-zinc-200",children:null!==(s=null==r?void 0:r.browser_hwaccel)&&void 0!==s?s:"—"})]}),(0,n.jsxs)("p",{children:[(0,n.jsxs)("span",{className:"text-zinc-600",children:[t_("稳定模式","Stability"),": "]}),(0,n.jsx)("span",{className:"text-amber-200/90",children:null!==(l=null==r?void 0:r.stability_mode)&&void 0!==l?l:"—"})]})]}),((null==r?void 0:r.degradation_reasons)||[]).length?(0,n.jsx)("ul",{className:"mt-2 list-inside list-disc space-y-1 text-[10px] text-amber-200/80",children:((null==r?void 0:r.degradation_reasons)||[]).map(e=>(0,n.jsx)("li",{children:e},e))}):null]})})()]})]})]}):null,tb?(0,n.jsxs)("div",{className:"rounded-2xl border p-5 ".concat(tb.ready?"border-emerald-500/25 bg-gradient-to-br from-emerald-500/[0.08] via-zinc-950/50 to-cyan-500/[0.05]":"border-amber-500/30 bg-gradient-to-br from-amber-500/[0.1] via-zinc-950/55 to-rose-500/[0.05]"),children:[(0,n.jsxs)("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:t_("安装验收","Deployment health")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:t_("一键脚本完成后,本机 API、局域网入口、反向代理与 mDNS 的可用性检查。","Post-install checks for loopback API, LAN access, reverse proxy, and mDNS.")})]}),(0,n.jsx)("span",{className:"rounded-full px-3 py-1 text-[10px] font-semibold ".concat(tb.ready?"bg-emerald-500/20 text-emerald-100 ring-1 ring-emerald-500/30":"bg-amber-500/20 text-amber-100 ring-1 ring-amber-500/30"),children:tb.ready?t_("可交付","Ready"):t_("需处理","Needs attention")})]}),(0,n.jsxs)("div",{className:"mt-4 grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:t_("验收摘要","Acceptance summary")}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-sm text-white",children:[null!==(eP=tb.pass_count)&&void 0!==eP?eP:0,"/",null!==(eL=tb.total_count)&&void 0!==eL?eL:0,(0,n.jsx)("span",{className:"ml-2 text-[11px] text-zinc-500",children:t_("已通过","checks passed")})]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[t_("必需项","Required"),": ",null!==(eM=tb.required_total)&&void 0!==eM?eM:0," \xb7 mDNS"," ",tb.mdns_enabled?t_("已启用","enabled"):t_("已关闭","disabled")," \xb7 edge"," ",tb.live_edge_enabled?t_("已启用","enabled"):t_("已关闭","disabled")]}),(0,n.jsx)("div",{className:"mt-3 flex flex-wrap gap-2",children:Object.entries(tb.urls||{}).filter(e=>{let[,t]=e;return!!t}).slice(0,4).map(e=>{let[t,s]=e;return(0,n.jsx)("a",{href:(0,ex.p)(s),target:"_blank",rel:"noreferrer",className:"rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-[11px] text-zinc-200 ring-1 ring-white/10",children:t},t)})})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/30 p-4",children:[(0,n.jsx)("p",{className:"text-[10px] font-semibold uppercase tracking-wider text-zinc-500",children:(null===(X=tb.failing_checks)||void 0===X?void 0:X.length)?t_("失败项","Failing checks"):t_("当前状态","Current state")}),(null===($=tb.failing_checks)||void 0===$?void 0:$.length)?(0,n.jsx)("ul",{className:"mt-2 space-y-2 text-[11px] text-zinc-300",children:tb.failing_checks.slice(0,4).map(e=>(0,n.jsxs)("li",{className:"rounded-lg border border-amber-500/20 bg-amber-500/10 px-3 py-2",children:[(0,n.jsx)("span",{className:"font-semibold text-amber-100",children:e.label}),(0,n.jsx)("span",{className:"ml-2 text-zinc-400",children:e.detail||"—"})]},e.id))}):(0,n.jsx)("p",{className:"mt-2 text-[11px] text-emerald-200/90",children:t_("必需检查已全部通过,可直接用浏览器面板交付。","All required checks pass; browser control plane is ready.")}),(tb.hints||[]).length?(0,n.jsx)("ul",{className:"mt-3 list-inside list-disc space-y-1 text-[10px] text-zinc-400",children:(tb.hints||[]).slice(0,3).map(e=>(0,n.jsx)("li",{children:e},e))}):null]})]})]}):null,(null==eJ?void 0:null===(Q=eJ.stack)||void 0===Q?void 0:Q.neko_login)?(0,n.jsxs)("div",{className:"rounded-2xl border border-fuchsia-500/25 bg-gradient-to-r from-fuchsia-500/[0.08] to-violet-500/[0.06] p-5",children:[(0,n.jsxs)("h3",{className:"text-sm font-semibold text-white",children:["Neko ",t_("登录","sign-in")]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:eJ.stack.neko_login.note_zh||t_(tE?"Neko Chromium ".concat(tE," 使用下列账号策略。"):"Neko Chromium 多实例使用下列账号策略。",tE?"Neko Chromium ".concat(tE," uses the policy below."):"Neko Chromium instances use the policy below.")}),(0,n.jsx)("p",{className:"mt-2 text-[11px] text-amber-200/85",children:t_("重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678)。用错密码会像一直加载;勿把「用户名/密码」写在一行。","Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.")}),tU.length?(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:tU.map(e=>(0,n.jsxs)("a",{href:e.url,target:"_blank",rel:"noreferrer",className:"rounded-lg border border-fuchsia-400/20 bg-black/25 px-3 py-2 text-[11px] text-fuchsia-100 ring-1 ring-fuchsia-400/20",children:[e.label,e.port?" :".concat(e.port):""]},e.key))}):null,(0,n.jsxs)("div",{className:"mt-4 grid gap-3 sm:grid-cols-2 font-mono text-sm",children:[(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/40 px-4 py-3",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase text-zinc-500",children:t_("昵称示例","Display Name Example")}),(0,n.jsx)("p",{className:"mt-1 text-fuchsia-200",children:null!==(eT=eJ.stack.neko_login.user_login)&&void 0!==eT?eT:"live"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] uppercase text-zinc-500",children:t_("连接口令","Access Password")}),(0,n.jsx)("p",{className:"mt-0.5 text-fuchsia-100",children:null!==(eU=eJ.stack.neko_login.user_password)&&void 0!==eU?eU:"—"})]}),(0,n.jsxs)("div",{className:"rounded-xl border border-white/10 bg-black/40 px-4 py-3",children:[(0,n.jsx)("p",{className:"text-[10px] uppercase text-zinc-500",children:t_("管理昵称示例","Admin Display Name Example")}),(0,n.jsx)("p",{className:"mt-1 text-violet-200",children:null!==(eE=eJ.stack.neko_login.admin_login)&&void 0!==eE?eE:"admin"}),(0,n.jsx)("p",{className:"mt-2 text-[10px] uppercase text-zinc-500",children:t_("管理口令","Admin Password")}),(0,n.jsx)("p",{className:"mt-0.5 text-violet-100",children:null!==(eI=eJ.stack.neko_login.admin_password)&&void 0!==eI?eI:"—"})]})]}),(0,n.jsx)("p",{className:"mt-3 text-[10px] text-zinc-600",children:t_("x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;默认保留管理员会话、开发者工具和 Tampermonkey 持久化。本栈未启用 Firefox 版 Neko。","x86 defaults to Neko’s Chrome image, ARM defaults to Chromium, and admin access, DevTools, plus persistent Tampermonkey stay enabled. This stack does not use Firefox Neko.")})]}):null,(null==eJ?void 0:null===(et=eJ.network)||void 0===et?void 0:null===(ee=et.interfaces)||void 0===ee?void 0:ee.length)?(0,n.jsxs)("div",{className:"rounded-2xl border border-cyan-500/15 bg-gradient-to-br from-cyan-500/[0.07] to-transparent p-5",children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold text-white",children:[(0,n.jsx)(k.Z,{className:"h-4 w-4 text-cyan-300"}),t_("网络流量(累计)","Network I/O (cumulative)")]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:t_("自开机以来各接口收发字节;非实时速率。","Per-interface RX/TX since boot (not a live bitrate).")}),(0,n.jsx)("div",{className:"mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3",children:(eJ.network.interfaces||[]).slice(0,6).map(e=>{var t,s,l,i;return(0,n.jsxs)("div",{className:"rounded-xl border border-white/[0.06] bg-black/25 px-4 py-3 font-mono text-[11px]",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-cyan-200/90",children:e.name}),(0,n.jsxs)("p",{className:"mt-2 text-zinc-500",children:[t_("链路","Link"),": ",e.operstate||"unknown"," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",e.speed_mbps?"".concat(e.speed_mbps," Mbps"):t_("未知速率","unknown speed")]}),(0,n.jsxs)("p",{className:"mt-2 text-zinc-500",children:["↓ ",e2(e.rx_bytes)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",null!==(l=null===(t=e.rx_packets)||void 0===t?void 0:t.toLocaleString())&&void 0!==l?l:"—"," pkt"]}),(0,n.jsxs)("p",{className:"mt-0.5 text-zinc-500",children:["↑ ",e2(e.tx_bytes)," ",(0,n.jsx)("span",{className:"text-zinc-600",children:"\xb7"})," ",null!==(i=null===(s=e.tx_packets)||void 0===s?void 0:s.toLocaleString())&&void 0!==i?i:"—"," pkt"]})]},e.name)})})]}):null,(null==eJ?void 0:null===(en=eJ.live_events)||void 0===en?void 0:null===(es=en.pm2_tail)||void 0===es?void 0:es.length)||(null==eJ?void 0:null===(ei=eJ.live_events)||void 0===ei?void 0:null===(el=ei.process_digest)||void 0===el?void 0:el.length)?(0,n.jsxs)("div",{className:"grid gap-4 lg:grid-cols-2",children:[(0,n.jsxs)("div",{className:"rounded-2xl border border-amber-500/15 bg-zinc-950/40 p-5",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:t_("直播 / PM2 事件","Live / PM2 feed")}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:t_("PM2 合并日志尾部","PM2 merged log tail")}),(0,n.jsx)("pre",{className:"mt-3 max-h-48 overflow-auto whitespace-pre-wrap rounded-lg border border-white/[0.05] bg-black/40 p-3 font-mono text-[10px] leading-relaxed text-zinc-400",children:((null==eJ?void 0:null===(ea=eJ.live_events)||void 0===ea?void 0:ea.pm2_tail)||[]).join("\n")||t_("暂无 PM2 输出","No PM2 output")})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/15 bg-zinc-950/40 p-5",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:t_("进程快照","Process digest")}),(0,n.jsx)("ul",{className:"mt-3 space-y-2",children:((null==eJ?void 0:null===(eo=eJ.live_events)||void 0===eo?void 0:eo.process_digest)||[]).map(e=>(0,n.jsxs)("li",{className:"flex items-center justify-between gap-2 rounded-lg border border-white/[0.05] bg-black/30 px-3 py-2 text-xs",children:[(0,n.jsx)("span",{className:"truncate text-zinc-300",children:e.label||e.pm2}),(0,n.jsx)("span",{className:"shrink-0 rounded-md px-2 py-0.5 text-[10px] font-medium ".concat(e3(e.status||"")),children:e4(e.status||"",t_)})]},e.pm2))})]})]}):null,(0,n.jsxs)("div",{className:"rounded-2xl border border-violet-500/20 bg-gradient-to-br from-violet-500/[0.12] via-zinc-950/50 to-fuchsia-500/[0.06] p-5 shadow-lg shadow-violet-500/5",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("h3",{className:"text-sm font-semibold text-white",children:t_("直播进程","Live processes")}),(0,n.jsx)("span",{className:"rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-medium text-zinc-300",children:(null==eJ?void 0:null===(ec=eJ.live)||void 0===ec?void 0:ec.backend_mode)||"—"})]}),(0,n.jsxs)("ul",{className:"mt-4 space-y-2.5",children:[((null==eJ?void 0:null===(ed=eJ.live)||void 0===ed?void 0:ed.processes)||[]).slice(0,10).map(e=>(0,n.jsxs)("li",{className:"group flex items-center gap-3 rounded-xl border border-white/[0.08] bg-black/35 px-3 py-2.5 transition hover:border-violet-400/25 hover:bg-black/45",children:[(0,n.jsx)("div",{className:"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/30 to-fuchsia-500/20 text-[10px] font-bold text-violet-100 ring-1 ring-white/10",children:"LIVE"}),(0,n.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,n.jsx)("p",{className:"truncate text-sm font-medium text-zinc-100",children:e.label||e.pm2}),(0,n.jsx)("p",{className:"truncate font-mono text-[10px] text-zinc-600",children:e.script||e.pm2})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-lg px-2.5 py-1 text-[10px] font-semibold ".concat(e3(e.process_status||"")),children:e4(e.process_status||"",t_)})]},e.pm2)),(null==eJ?void 0:null===(em=eJ.live)||void 0===em?void 0:null===(eu=em.processes)||void 0===eu?void 0:eu.length)?null:(0,n.jsx)("li",{className:"rounded-xl border border-dashed border-white/10 py-8 text-center text-xs text-zinc-600",children:t_("暂无直播进程","No live processes")})]}),(0,n.jsxs)("div",{className:"mt-5 grid grid-cols-2 gap-2",children:[(0,n.jsx)("button",{type:"button",onClick:()=>eF("live_youtube"),className:"rounded-xl border border-violet-400/35 bg-violet-500/15 py-2.5 text-[11px] font-semibold text-violet-100 shadow-inner shadow-black/20 transition hover:bg-violet-500/25",children:"YouTube"}),(0,n.jsx)("button",{type:"button",onClick:()=>eF("live_tiktok"),className:"rounded-xl border border-cyan-400/35 bg-cyan-500/15 py-2.5 text-[11px] font-semibold text-cyan-100 shadow-inner shadow-black/20 transition hover:bg-cyan-500/25",children:"TikTok"})]})]}),(null==eJ?void 0:eJ.probes)&&ty?(0,n.jsxs)("div",{className:"grid gap-3 sm:grid-cols-2 lg:grid-cols-3",children:[eJ.probes.srs?(()=>{var e,t,s,l,i,r,a;let o=null===(e=eJ.probes)||void 0===e?void 0:e.srs,c=null==o?void 0:null===(t=o.tcp)||void 0===t?void 0:t.reachable,d=null==o?void 0:null===(s=o.tcp_api)||void 0===s?void 0:s.reachable,x=null==o?void 0:o.http_ui,u=null==o?void 0:o.http,m=null==o?void 0:o.api_port,h=null!==(i=null==o?void 0:o.api_ok)&&void 0!==i?i:(null==u?void 0:u.http_code)===200,p=null==x?void 0:x.http_code;return(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-zinc-950/40 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-white",children:"SRS"}),(0,n.jsx)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat(h?"bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-1 ring-rose-500/25"),children:h?t_("API 正常","API OK"):t_("API 异常","API down")})]}),(0,n.jsxs)("p",{className:"mt-1 font-mono text-[11px] text-zinc-500",children:[":",null!==(r=null==o?void 0:o.port)&&void 0!==r?r:"—"," / API :",null!=m?m:"—"," \xb7 TCP ",c?"OK":"—"," \xb7 API TCP"," ",d?"OK":"—"]}),(0,n.jsxs)("p",{className:"mt-1 font-mono text-[11px] text-zinc-500",children:["API HTTP ",null!==(a=null==u?void 0:u.http_code)&&void 0!==a?a:"—"," \xb7 ",t_("根路径","root")," ",null!=p?p:"—",404===p?t_("(无首页不影响推流)"," (no index is OK for streaming)"):null]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[t_("延迟","RTT")," ",(null==u?void 0:u.latency_ms)!=null?"".concat(u.latency_ms," ms"):(null==o?void 0:null===(l=o.tcp)||void 0===l?void 0:l.latency_ms)!=null?"".concat(o.tcp.latency_ms," ms"):"—"]}),!h&&(null==u?void 0:u.error)?(0,n.jsx)("p",{className:"mt-1 line-clamp-2 text-[10px] text-amber-200/80",children:u.error}):null]})})():null,eJ.probes.neko?(()=>{var e,t,s,l,i,r,a;let o=null===(e=eJ.probes)||void 0===e?void 0:e.neko,c=null==o?void 0:null===(t=o.tcp)||void 0===t?void 0:t.reachable,d=null==o?void 0:o.http,x=(null==o?void 0:o.instances)||[],u=null!==(l=null==o?void 0:o.running_count)&&void 0!==l?l:x.filter(e=>{var t;return null===(t=e.tcp)||void 0===t?void 0:t.reachable}).length,m=null!==(i=null==o?void 0:o.required_count)&&void 0!==i?i:x.length;return(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.07] bg-zinc-950/40 p-4 backdrop-blur-sm",children:[(0,n.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,n.jsx)("p",{className:"text-xs font-semibold text-white",children:"Neko Chromium"}),(0,n.jsxs)("span",{className:"rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat(u>0?"bg-emerald-500/20 text-emerald-200 ring-1 ring-emerald-500/30":"bg-rose-500/15 text-rose-200 ring-1 ring-rose-500/25"),children:[u,"/",m||"—"," ",t_("在线","online")]})]}),(0,n.jsx)("p",{className:"mt-1 text-[11px] text-zinc-500",children:t_("Docker 内 Chromium(非 Chrome 品牌);arm64/x86 同一镜像。","Chromium in Docker (not Chrome branding); multi-arch.")}),(0,n.jsxs)("p",{className:"mt-2 font-mono text-[11px] text-zinc-500",children:[":",null!==(r=null==o?void 0:o.port)&&void 0!==r?r:"—"," \xb7 TCP ",c?"OK":"—"," \xb7 HTTP ",null!==(a=null==d?void 0:d.http_code)&&void 0!==a?a:"—"]}),(0,n.jsxs)("p",{className:"mt-1 text-[11px] text-zinc-500",children:[t_("延迟","RTT")," ",(null==d?void 0:d.latency_ms)!=null?"".concat(d.latency_ms," ms"):(null==o?void 0:null===(s=o.tcp)||void 0===s?void 0:s.latency_ms)!=null?"".concat(o.tcp.latency_ms," ms"):"—"]}),x.length?(0,n.jsx)("div",{className:"mt-3 space-y-2",children:x.map(e=>{var t,s,l,i,r,a,o;return(0,n.jsxs)("div",{className:"flex items-center justify-between gap-3 rounded-lg border border-white/[0.05] bg-black/25 px-3 py-2",children:[(0,n.jsxs)("div",{className:"min-w-0",children:[(0,n.jsxs)("p",{className:"text-[11px] font-semibold text-zinc-200",children:["Neko ",null!==(r=e.id)&&void 0!==r?r:"?"]}),(0,n.jsxs)("p",{className:"font-mono text-[10px] text-zinc-500",children:[":",null!==(a=e.port)&&void 0!==a?a:"—"," \xb7 TCP ",(null===(t=e.tcp)||void 0===t?void 0:t.reachable)?"OK":"—"," \xb7 HTTP"," ",null!==(o=null===(s=e.http)||void 0===s?void 0:s.http_code)&&void 0!==o?o:"—"]})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold ".concat((null===(l=e.tcp)||void 0===l?void 0:l.reachable)?"bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-500/25":"bg-zinc-500/15 text-zinc-400 ring-1 ring-zinc-500/25"),children:(null===(i=e.tcp)||void 0===i?void 0:i.reachable)?t_("运行中","Running"):t_("未连通","Offline")})]},"".concat(e.id,"-").concat(e.port))})}):null,!(null==d?void 0:d.reachable)&&(null==d?void 0:d.error)?(0,n.jsx)("p",{className:"mt-1 line-clamp-2 text-[10px] text-amber-200/80",children:d.error}):null]})})():null]}):null]}):null,"services"===eK?(0,n.jsxs)("div",{className:"grid gap-4 md:grid-cols-2 xl:grid-cols-3",children:[((null==eJ?void 0:eJ.services)||[]).map((e,t)=>(0,n.jsxs)(l.E.div,{initial:{opacity:0,y:10},animate:{opacity:1,y:0},transition:{delay:.04*t},whileHover:{y:-2},className:"rounded-2xl border border-white/[0.07] bg-gradient-to-b from-white/[0.05] to-transparent p-5",children:[(0,n.jsxs)("div",{className:"flex items-start justify-between gap-2",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("h3",{className:"font-semibold text-white",children:e.label}),(0,n.jsx)("p",{className:"mt-1 text-xs text-zinc-500 line-clamp-2",children:e.description})]}),(0,n.jsx)("span",{className:"shrink-0 rounded-full px-2.5 py-1 text-[10px] font-semibold ".concat(e3(e.running?"online":e.status)),children:e.running?t_("运行中","Running"):e.status})]}),e.detail?(0,n.jsx)("p",{className:"mt-2 text-[11px] leading-snug line-clamp-6 ".concat("skipped"===e.status?"text-slate-300/90":"text-amber-200/85"),children:e.detail}):null,"shellcrash"===e.service_id&&e.available&&!e.running?(0,n.jsx)("p",{className:"mt-2 text-[11px] text-zinc-500",children:t_("stopped 为常态:仅当需要透明代理 / 规则分流时再点 Start(或 systemctl enable --now shellcrash)。","Stopped is normal; Start only when you need the proxy (or enable shellcrash.service).")}):null,(0,n.jsxs)("div",{className:"mt-4 flex flex-wrap items-center gap-2",children:[e.url?(0,n.jsx)("a",{href:(0,ex.p)(e.url),target:"_blank",rel:"noreferrer",className:"rounded-lg bg-violet-500/15 px-3 py-1.5 text-xs font-medium text-violet-200 ring-1 ring-violet-500/25",children:t_("打开面板","Open panel")}):null,(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"start"),className:"rounded-lg bg-emerald-500/20 px-3 py-1.5 text-xs font-medium text-emerald-200 ring-1 ring-emerald-500/30 disabled:opacity-30",children:"Start"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"install"),className:"rounded-lg bg-sky-500/15 px-3 py-1.5 text-xs font-medium text-sky-200 ring-1 ring-sky-500/25 disabled:opacity-30",children:"Install"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"restart"),className:"rounded-lg bg-amber-500/15 px-3 py-1.5 text-xs font-medium text-amber-200 ring-1 ring-amber-500/25 disabled:opacity-30",children:"Restart"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"upgrade"),className:"rounded-lg bg-indigo-500/15 px-3 py-1.5 text-xs font-medium text-indigo-200 ring-1 ring-indigo-500/25 disabled:opacity-30",children:"Upgrade"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"stop"),className:"rounded-lg bg-rose-500/15 px-3 py-1.5 text-xs font-medium text-rose-200 ring-1 ring-rose-500/25 disabled:opacity-30",children:"Stop"}),(0,n.jsx)("button",{type:"button",disabled:!!eQ||!e.available||"android_gateway"===e.service_id&&"skipped"===e.status,title:"android_gateway"===e.service_id&&"skipped"===e.status?t_("单网口无需网关","Not needed on single NIC"):e.available?void 0:e.detail||t_("当前不可操作","Unavailable"),onClick:()=>void tI(e.service_id,"uninstall"),className:"rounded-lg bg-zinc-500/15 px-3 py-1.5 text-xs font-medium text-zinc-200 ring-1 ring-zinc-500/25 disabled:opacity-30",children:"Uninstall"})]})]},e.service_id)),(null==eJ?void 0:null===(eh=eJ.services)||void 0===eh?void 0:eh.length)?null:(0,n.jsx)("p",{className:"text-sm text-zinc-500",children:t_("暂无服务数据","No services")})]}):null,"live_youtube"===eK?(0,n.jsx)(e0,{t:t_,busy:eQ,entries:ts,currentProc:tr,setCurrentProc:ta,urlConfig:to,setUrlConfig:tc,onRunProcess:(e,t)=>void tA(e,t),onSaveUrlConfig:(e,t)=>tD(e,t),fetchJson:tR,liveProcesses:tl.length?tl:null!==(eA=null==eJ?void 0:null===(ep=eJ.live)||void 0===ep?void 0:ep.processes)&&void 0!==eA?eA:[],notify:tC,quickLinks:tT,onNavigate:e=>eF(e)}):null,"live_tiktok"===eK?(0,n.jsx)(ez,{t:t_,busy:!!eQ,entries:ts,currentProc:tr,setCurrentProc:ta,urlConfig:to,setUrlConfig:tc,onRunProcess:e=>void tA(e),onSaveUrlConfig:e=>tD(e),onReplayGoLive:async(e,t)=>{let s=t.replace(/[,,|]/g,"_");await tD("原画,".concat(e,",主播: ").concat(s)),await tA("start")},onCopy:tH,fetchJson:tR,apiPrefix:eD,quickLinks:tT,hdmiStatus:null==eJ?void 0:eJ.hdmi,notify:tC,onNavigate:e=>eF(e)}):null,"android"===eK?(0,n.jsx)(er,{t:t_,notify:tC,fetchJson:tR,apiBase:eD,busy:eQ,androidSerial:tu,setAndroidSerial:tm,devices:(null==eJ?void 0:null===(eb=eJ.android)||void 0===eb?void 0:eb.devices)||[],adbAvailable:!!(null==eJ?void 0:null===(eg=eJ.android)||void 0===eg?void 0:eg.adb_available),apkPath:td,setApkPath:tx,onAndroidAction:(e,t)=>tZ(e,null!=t?t:{}),onCopy:tH,onRefreshDevices:()=>void tP()}):null,"network"===eK?(0,n.jsxs)("div",{className:"mx-auto max-w-7xl space-y-6",children:[(0,n.jsxs)("div",{className:"rounded-2xl border p-6 ".concat("light"===eH?"border-violet-200/90 bg-gradient-to-br from-violet-50 via-white to-fuchsia-50/80":"border-violet-500/25 bg-gradient-to-br from-violet-500/10 via-zinc-950/40 to-fuchsia-500/5"),children:[(0,n.jsxs)("h3",{className:"flex items-center gap-2 text-sm font-semibold ".concat("light"===eH?"text-slate-900":"text-white"),children:[(0,n.jsx)(k.Z,{className:"h-4 w-4 ".concat("light"===eH?"text-violet-600":"text-violet-300")}),t_("ShellCrash 网络面板","ShellCrash panel")]}),(0,n.jsx)("p",{className:"mt-2 text-xs ".concat("light"===eH?"text-slate-600":"text-zinc-400"),children:t_("透明代理与规则分流直接在本页完成,无需跳转;流媒体与其它系统服务请在「服务」页管理。","Proxy rules and YAML editing are embedded here. Use Services for SRS/Neko/Chromium, etc.")}),(0,n.jsx)("div",{className:"mt-6 flex flex-col items-center gap-4 sm:flex-row sm:justify-center",children:[{tx:t_("互联网","Internet"),c:"from-cyan-400 to-blue-500"},{tx:"ShellCrash",c:"from-violet-500 to-fuchsia-500"},{tx:t_("本机 / 安卓","Host / Android"),c:"from-emerald-400 to-teal-500"}].map((e,t)=>(0,n.jsxs)("div",{className:"flex items-center gap-4",children:[(0,n.jsx)("div",{className:"flex h-16 w-28 items-center justify-center rounded-2xl bg-gradient-to-br ".concat(e.c," text-xs font-bold text-white shadow-lg"),children:e.tx}),t<2?(0,n.jsx)(h.Z,{className:"hidden h-5 w-5 sm:block ".concat("light"===eH?"text-slate-400":"text-zinc-600")}):null]},e.tx))})]}),(0,n.jsx)("div",{className:"overflow-hidden rounded-2xl border shadow-xl ".concat("light"===eH?"border-slate-200/90 bg-white shadow-slate-200/40":"border-white/[0.08] bg-zinc-950/50 shadow-black/30"),children:(0,n.jsx)("div",{className:"max-h-[min(78vh,56rem)] min-h-[22rem] overflow-y-auto overflow-x-hidden",children:(0,n.jsx)(P.d,{embeddedShellCrash:!0,portalLang:eZ})})})]}):null,"settings"===eK?(0,n.jsxs)("div",{className:"mx-auto max-w-lg space-y-6",children:[(0,n.jsx)("p",{className:"text-sm ".concat("light"===eH?"text-slate-600":"text-zinc-400"),children:t_("常用选项(本机偏好);完整文件与 JSON 编辑见下方链接。","Local preferences; full file/JSON editor via the link below.")}),(0,n.jsxs)("div",{className:"space-y-4 rounded-2xl border border-white/[0.08] bg-zinc-950/50 p-5",children:[(0,n.jsxs)("label",{className:"flex cursor-pointer items-center justify-between gap-4",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-sm font-medium text-white",children:t_("总览自动刷新","Auto-refresh dashboard")}),(0,n.jsx)("p",{className:"text-[11px] text-zinc-500",children:"10s"})]}),(0,n.jsx)("input",{type:"checkbox",className:"h-5 w-5 accent-violet-500",checked:tf,onChange:e=>{let t=e.target.checked;tv(t);try{localStorage.setItem("livehub.autorefresh",t?"1":"0")}catch(e){}tC(t?t_("已开启刷新","Auto refresh on"):t_("已暂停刷新","Paused"))}})]}),(0,n.jsxs)("label",{className:"flex cursor-pointer items-center justify-between gap-4 border-t border-white/[0.06] pt-4",children:[(0,n.jsxs)("div",{children:[(0,n.jsx)("p",{className:"text-sm font-medium text-white",children:t_("总览显示 SRS / Neko 探针","Show SRS/Neko probes")}),(0,n.jsx)("p",{className:"text-[11px] text-zinc-500",children:t_("关闭可精简首页","Hide stream probe cards")})]}),(0,n.jsx)("input",{type:"checkbox",className:"h-5 w-5 accent-violet-500",checked:ty,onChange:e=>{let t=e.target.checked;tw(t);try{localStorage.setItem("livehub.showStreamProbes",t?"1":"0")}catch(e){}}})]})]}),(0,n.jsxs)("div",{className:"rounded-2xl border border-white/[0.08] bg-zinc-950/40 p-5",children:[(0,n.jsx)("p",{className:"text-xs font-semibold uppercase tracking-wider text-zinc-500",children:t_("配置快照","Config snapshot")}),(0,n.jsx)("p",{className:"mt-2 font-mono text-[11px] text-zinc-400",children:String((null==th?void 0:th.config_root)||"—")}),(0,n.jsxs)("p",{className:"mt-3 text-sm text-zinc-300",children:[t_("主机名","Hostname"),":"," ",(0,n.jsx)("span",{className:"font-mono text-violet-200",children:(()=>{var e,t,s;let n=null==th?void 0:th.system;return(null==n?void 0:null===(t=n.identity)||void 0===t?void 0:null===(e=t.hostname_alias)||void 0===e?void 0:e.trim())||((null==eJ?void 0:null===(s=eJ.stack)||void 0===s?void 0:s.mdns_host)||"").replace(/\.local$/i,"")||"—"})()})]}),(0,n.jsx)("div",{className:"mt-4 flex flex-wrap gap-2",children:null==th?void 0:null===(ef=th.services)||void 0===ef?void 0:ef.slice(0,8).map(e=>(0,n.jsxs)("span",{className:"rounded-full px-2.5 py-1 text-[10px] font-medium ".concat(e.enabled?"bg-emerald-500/15 text-emerald-200 ring-1 ring-emerald-500/25":"bg-zinc-500/15 text-zinc-400"),children:[e.id," ",e.enabled?"ON":"off"]},e.id))})]}),(0,n.jsxs)("a",{href:"/console?tab=config",className:"inline-flex w-full items-center justify-center gap-2 rounded-xl border py-3 text-sm transition ".concat("light"===eH?"border-slate-200 bg-slate-50 text-violet-700 hover:bg-slate-100":"border-white/10 bg-white/[0.05] text-violet-200 hover:bg-white/[0.08]"),children:[t_("完整配置中心(文件 / JSON)","Full config center (files / JSON)"),(0,n.jsx)(z.Z,{className:"h-4 w-4"})]})]}):null]},eK)})})]})]}),(0,n.jsx)(i.M,{children:eW?(0,n.jsx)(l.E.div,{initial:{opacity:0},animate:{opacity:1},exit:{opacity:0},className:"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm lg:hidden",onClick:()=>eY(!1),children:(0,n.jsxs)(l.E.aside,{initial:{x:-280},animate:{x:0},exit:{x:-280},transition:{type:"spring",damping:28,stiffness:320},className:"absolute left-0 top-0 h-full w-[min(88vw,280px)] border-r border-white/10 bg-zinc-950 p-4 shadow-2xl",onClick:e=>e.stopPropagation(),children:[(0,n.jsx)("div",{className:"flex justify-end",children:(0,n.jsx)("button",{type:"button",onClick:()=>eY(!1),className:"rounded-lg p-2",children:(0,n.jsx)(S.Z,{className:"h-5 w-5"})})}),(0,n.jsx)("nav",{className:"mt-4 flex flex-col gap-1",children:tO.map(e=>{let t=e.icon,s=eK===e.id;return(0,n.jsxs)("button",{type:"button",onClick:()=>{eF(e.id),eY(!1)},"aria-current":s?"page":void 0,className:"flex items-center gap-3 rounded-xl px-3 py-3 text-left text-sm transition ".concat(s?"bg-white/[0.08] text-white shadow-inner shadow-white/5":"text-zinc-300 hover:bg-white/[0.04]"),children:[(0,n.jsx)(t,{className:"h-4 w-4 ".concat(s?"text-violet-300":"text-violet-400")}),t_(e.labelZh,e.labelEn)]},e.id)})})]})}):null})]})}}},function(e){e.O(0,[500,719,246,971,117,744],function(){return e(e.s=4578)}),_N_E=e.O()}]); \ No newline at end of file diff --git a/d2ypp2/web-console/out/_next/static/css/f9347d040ae16366.css b/d2ypp2/web-console/out/_next/static/css/693f7950f3222bce.css similarity index 55% rename from d2ypp2/web-console/out/_next/static/css/f9347d040ae16366.css rename to d2ypp2/web-console/out/_next/static/css/693f7950f3222bce.css index c294002..a1aa3c0 100644 --- a/d2ypp2/web-console/out/_next/static/css/f9347d040ae16366.css +++ b/d2ypp2/web-console/out/_next/static/css/693f7950f3222bce.css @@ -1,3 +1,3 @@ @font-face{font-family:__geistSans_1e4310;src:url(/_next/static/media/4473ecc91f70f139-s.p.woff) format("woff");font-display:swap;font-weight:100 900}@font-face{font-family:__geistSans_Fallback_1e4310;src:local("Arial");ascent-override:85.83%;descent-override:20.52%;line-gap-override:9.33%;size-adjust:107.19%}.__className_1e4310{font-family:__geistSans_1e4310,__geistSans_Fallback_1e4310}.__variable_1e4310{--font-geist-sans:"__geistSans_1e4310","__geistSans_Fallback_1e4310"}@font-face{font-family:__geistMono_c3aa02;src:url(/_next/static/media/463dafcda517f24f-s.p.woff) format("woff");font-display:swap;font-weight:100 900}@font-face{font-family:__geistMono_Fallback_c3aa02;src:local("Arial");ascent-override:69.97%;descent-override:16.73%;line-gap-override:7.61%;size-adjust:131.49%}.__className_c3aa02{font-family:__geistMono_c3aa02,__geistMono_Fallback_c3aa02}.__variable_c3aa02{--font-geist-mono:"__geistMono_c3aa02","__geistMono_Fallback_c3aa02"}*,:after,:before{--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:rgba(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:rgba(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: }/* ! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com -*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-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]){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,pre,samp{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,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{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}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-bottom-0\.5{bottom:-.125rem}.-left-1\/4{left:-25%}.-right-0\.5{right:-.125rem}.-right-20{right:-5rem}.-right-4{right:-1rem}.-top-20{top:-5rem}.-top-4{top:-1rem}.bottom-0{bottom:0}.bottom-24{bottom:6rem}.bottom-3{bottom:.75rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.col-span-2{grid-column:span 2/span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.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}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-6{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:6}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-11{height:2.75rem}.h-12{height:3rem}.h-16{height:4rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-36{height:9rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-9{height:2.25rem}.h-\[30rem\]{height:30rem}.h-\[36rem\]{height:36rem}.h-\[42rem\]{height:42rem}.h-\[min\(72vh\2c 40rem\)\]{height:min(72vh,40rem)}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-\[28rem\]{max-height:28rem}.max-h-\[min\(22rem\2c 42vh\)\]{max-height:min(22rem,42vh)}.max-h-\[min\(24rem\2c 55vh\)\]{max-height:min(24rem,55vh)}.max-h-\[min\(70vh\2c 48rem\)\]{max-height:min(70vh,48rem)}.max-h-\[min\(72vh\2c 680px\)\]{max-height:min(72vh,680px)}.max-h-\[min\(78vh\2c 56rem\)\]{max-height:min(78vh,56rem)}.max-h-\[min\(78vh\2c 720px\)\]{max-height:min(78vh,720px)}.min-h-0{min-height:0}.min-h-\[100px\]{min-height:100px}.min-h-\[12rem\]{min-height:12rem}.min-h-\[14rem\]{min-height:14rem}.min-h-\[160px\]{min-height:160px}.min-h-\[18rem\]{min-height:18rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-\[200px\]{min-height:200px}.min-h-\[22rem\]{min-height:22rem}.min-h-\[280px\]{min-height:280px}.min-h-\[2rem\]{min-height:2rem}.min-h-\[32rem\]{min-height:32rem}.min-h-\[3rem\]{min-height:3rem}.min-h-\[4rem\]{min-height:4rem}.min-h-\[6rem\]{min-height:6rem}.min-h-\[72px\]{min-height:72px}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-9{width:2.25rem}.w-\[36rem\]{width:36rem}.w-\[42rem\]{width:42rem}.w-\[min\(88vw\2c 280px\)\]{width:min(88vw,280px)}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[12rem\]{min-width:12rem}.min-w-\[1rem\]{min-width:1rem}.min-w-\[6rem\]{min-width:6rem}.min-w-\[7rem\]{min-width:7rem}.min-w-\[9rem\]{min-width:9rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[min\(92vw\2c 28rem\)\]{max-width:min(92vw,28rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.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-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.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{column-gap:.5rem}.gap-y-0\.5{row-gap:.125rem}.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-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.625rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.625rem * 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-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * 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}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[1\.35rem\]{border-radius:1.35rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-xl{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.border{border-width:1px}.border-\[10px\]{border-width:10px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/50{border-color:rgba(251,191,36,.5)}.border-amber-500\/15{border-color:rgba(245,158,11,.15)}.border-amber-500\/20{border-color:rgba(245,158,11,.2)}.border-amber-500\/25{border-color:rgba(245,158,11,.25)}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-cyan-400\/35{border-color:rgba(34,211,238,.35)}.border-cyan-400\/50{border-color:rgba(34,211,238,.5)}.border-cyan-500\/15{border-color:rgba(6,182,212,.15)}.border-cyan-500\/20{border-color:rgba(6,182,212,.2)}.border-cyan-500\/25{border-color:rgba(6,182,212,.25)}.border-cyan-500\/30{border-color:rgba(6,182,212,.3)}.border-cyan-500\/40{border-color:rgba(6,182,212,.4)}.border-cyan-500\/50{border-color:rgba(6,182,212,.5)}.border-emerald-400\/30{border-color:rgba(52,211,153,.3)}.border-emerald-500\/15{border-color:rgba(16,185,129,.15)}.border-emerald-500\/20{border-color:rgba(16,185,129,.2)}.border-emerald-500\/25{border-color:rgba(16,185,129,.25)}.border-emerald-500\/30{border-color:rgba(16,185,129,.3)}.border-emerald-500\/50{border-color:rgba(16,185,129,.5)}.border-fuchsia-200\/80{border-color:rgba(245,208,254,.8)}.border-fuchsia-400\/20{border-color:rgba(232,121,249,.2)}.border-fuchsia-500\/20{border-color:rgba(217,70,239,.2)}.border-fuchsia-500\/25{border-color:rgba(217,70,239,.25)}.border-fuchsia-500\/30{border-color:rgba(217,70,239,.3)}.border-indigo-400\/50{border-color:rgba(129,140,248,.5)}.border-indigo-500\/25{border-color:rgba(99,102,241,.25)}.border-rose-500\/15{border-color:rgba(244,63,94,.15)}.border-rose-500\/20{border-color:rgba(244,63,94,.2)}.border-rose-500\/25{border-color:rgba(244,63,94,.25)}.border-rose-500\/30{border-color:rgba(244,63,94,.3)}.border-sky-500\/20{border-color:rgba(14,165,233,.2)}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-200\/80{border-color:rgba(226,232,240,.8)}.border-slate-200\/90{border-color:rgba(226,232,240,.9)}.border-slate-500\/30{border-color:rgba(100,116,139,.3)}.border-slate-700\/30{border-color:rgba(51,65,85,.3)}.border-violet-200\/90{border-color:rgba(221,214,254,.9)}.border-violet-400\/35{border-color:rgba(167,139,250,.35)}.border-violet-500\/15{border-color:rgba(139,92,246,.15)}.border-violet-500\/20{border-color:rgba(139,92,246,.2)}.border-violet-500\/25{border-color:rgba(139,92,246,.25)}.border-violet-500\/30{border-color:rgba(139,92,246,.3)}.border-violet-500\/35{border-color:rgba(139,92,246,.35)}.border-violet-500\/45{border-color:rgba(139,92,246,.45)}.border-violet-500\/50{border-color:rgba(139,92,246,.5)}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.border-white\/15{border-color:hsla(0,0%,100%,.15)}.border-white\/20{border-color:hsla(0,0%,100%,.2)}.border-white\/5{border-color:hsla(0,0%,100%,.05)}.border-white\/\[0\.04\]{border-color:hsla(0,0%,100%,.04)}.border-white\/\[0\.05\]{border-color:hsla(0,0%,100%,.05)}.border-white\/\[0\.06\]{border-color:hsla(0,0%,100%,.06)}.border-white\/\[0\.07\]{border-color:hsla(0,0%,100%,.07)}.border-white\/\[0\.08\]{border-color:hsla(0,0%,100%,.08)}.border-zinc-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.bg-\[\#050508\]{--tw-bg-opacity:1;background-color:rgb(5 5 8/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-amber-500\/15{background-color:rgba(245,158,11,.15)}.bg-amber-500\/20{background-color:rgba(245,158,11,.2)}.bg-amber-500\/5{background-color:rgba(245,158,11,.05)}.bg-amber-500\/\[0\.06\]{background-color:rgba(245,158,11,.06)}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/15{background-color:rgba(0,0,0,.15)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/35{background-color:rgba(0,0,0,.35)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-black\/45{background-color:rgba(0,0,0,.45)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/55{background-color:rgba(0,0,0,.55)}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-black\/75{background-color:rgba(0,0,0,.75)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-cyan-100{--tw-bg-opacity:1;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.bg-cyan-400\/10{background-color:rgba(34,211,238,.1)}.bg-cyan-500\/10{background-color:rgba(6,182,212,.1)}.bg-cyan-500\/15{background-color:rgba(6,182,212,.15)}.bg-cyan-500\/20{background-color:rgba(6,182,212,.2)}.bg-cyan-500\/30{background-color:rgba(6,182,212,.3)}.bg-cyan-500\/90{background-color:rgba(6,182,212,.9)}.bg-cyan-500\/\[0\.05\]{background-color:rgba(6,182,212,.05)}.bg-cyan-500\/\[0\.06\]{background-color:rgba(6,182,212,.06)}.bg-cyan-600{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-cyan-600\/80{background-color:rgba(8,145,178,.8)}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-emerald-500\/20{background-color:rgba(16,185,129,.2)}.bg-emerald-500\/90{background-color:rgba(16,185,129,.9)}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-fuchsia-500\/10{background-color:rgba(217,70,239,.1)}.bg-fuchsia-500\/20{background-color:rgba(217,70,239,.2)}.bg-indigo-500\/15{background-color:rgba(99,102,241,.15)}.bg-rose-500\/10{background-color:rgba(244,63,94,.1)}.bg-rose-500\/15{background-color:rgba(244,63,94,.15)}.bg-rose-600{--tw-bg-opacity:1;background-color:rgb(225 29 72/var(--tw-bg-opacity,1))}.bg-rose-950\/20{background-color:rgba(76,5,25,.2)}.bg-rose-950\/25{background-color:rgba(76,5,25,.25)}.bg-sky-500\/15{background-color:rgba(14,165,233,.15)}.bg-sky-500\/25{background-color:rgba(14,165,233,.25)}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-slate-500\/10{background-color:rgba(100,116,139,.1)}.bg-slate-500\/15{background-color:rgba(100,116,139,.15)}.bg-slate-800\/50{background-color:rgba(30,41,59,.5)}.bg-slate-950\/30{background-color:rgba(2,6,23,.3)}.bg-slate-950\/50{background-color:rgba(2,6,23,.5)}.bg-slate-950\/60{background-color:rgba(2,6,23,.6)}.bg-slate-950\/80{background-color:rgba(2,6,23,.8)}.bg-slate-950\/95{background-color:rgba(2,6,23,.95)}.bg-violet-100{--tw-bg-opacity:1;background-color:rgb(237 233 254/var(--tw-bg-opacity,1))}.bg-violet-100\/90{background-color:rgba(237,233,254,.9)}.bg-violet-400\/15{background-color:rgba(167,139,250,.15)}.bg-violet-500\/10{background-color:rgba(139,92,246,.1)}.bg-violet-500\/15{background-color:rgba(139,92,246,.15)}.bg-violet-500\/20{background-color:rgba(139,92,246,.2)}.bg-violet-500\/25{background-color:rgba(139,92,246,.25)}.bg-violet-500\/\[0\.06\]{background-color:rgba(139,92,246,.06)}.bg-violet-500\/\[0\.1\]{background-color:rgba(139,92,246,.1)}.bg-violet-600{--tw-bg-opacity:1;background-color:rgb(124 58 237/var(--tw-bg-opacity,1))}.bg-violet-600\/20{background-color:rgba(124,58,237,.2)}.bg-violet-600\/70{background-color:rgba(124,58,237,.7)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/10{background-color:hsla(0,0%,100%,.1)}.bg-white\/5{background-color:hsla(0,0%,100%,.05)}.bg-white\/70{background-color:hsla(0,0%,100%,.7)}.bg-white\/75{background-color:hsla(0,0%,100%,.75)}.bg-white\/80{background-color:hsla(0,0%,100%,.8)}.bg-white\/90{background-color:hsla(0,0%,100%,.9)}.bg-white\/\[0\.04\]{background-color:hsla(0,0%,100%,.04)}.bg-white\/\[0\.05\]{background-color:hsla(0,0%,100%,.05)}.bg-white\/\[0\.06\]{background-color:hsla(0,0%,100%,.06)}.bg-white\/\[0\.08\]{background-color:hsla(0,0%,100%,.08)}.bg-zinc-500\/15{background-color:hsla(240,4%,46%,.15)}.bg-zinc-600\/20{background-color:rgba(82,82,91,.2)}.bg-zinc-600\/30{background-color:rgba(82,82,91,.3)}.bg-zinc-700\/40{background-color:rgba(63,63,70,.4)}.bg-zinc-700\/90{background-color:rgba(63,63,70,.9)}.bg-zinc-950{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1))}.bg-zinc-950\/30{background-color:rgba(9,9,11,.3)}.bg-zinc-950\/40{background-color:rgba(9,9,11,.4)}.bg-zinc-950\/50{background-color:rgba(9,9,11,.5)}.bg-zinc-950\/60{background-color:rgba(9,9,11,.6)}.bg-zinc-950\/70{background-color:rgba(9,9,11,.7)}.bg-zinc-950\/80{background-color:rgba(9,9,11,.8)}.bg-zinc-950\/95{background-color:rgba(9,9,11,.95)}.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-400{--tw-gradient-from:#fbbf24 var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,191,36,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-50\/90{--tw-gradient-from:rgba(255,251,235,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(255,251,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/35{--tw-gradient-from:rgba(245,158,11,.35) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.07\]{--tw-gradient-from:rgba(245,158,11,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.12\]{--tw-gradient-from:rgba(245,158,11,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.1\]{--tw-gradient-from:rgba(245,158,11,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-400{--tw-gradient-from:#22d3ee var(--tw-gradient-from-position);--tw-gradient-to:rgba(34,211,238,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/35{--tw-gradient-from:rgba(6,182,212,.35) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.07\]{--tw-gradient-from:rgba(6,182,212,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.08\]{--tw-gradient-from:rgba(6,182,212,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.14\]{--tw-gradient-from:rgba(6,182,212,.14) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.1\]{--tw-gradient-from:rgba(6,182,212,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-400{--tw-gradient-from:#34d399 var(--tw-gradient-from-position);--tw-gradient-to:rgba(52,211,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-400\/30{--tw-gradient-from:rgba(52,211,153,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(52,211,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.07\]{--tw-gradient-from:rgba(16,185,129,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.08\]{--tw-gradient-from:rgba(16,185,129,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.12\]{--tw-gradient-from:rgba(16,185,129,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-50\/90{--tw-gradient-from:rgba(253,244,255,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(253,244,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-500\/10{--tw-gradient-from:rgba(217,70,239,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(217,70,239,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-500\/\[0\.08\]{--tw-gradient-from:rgba(217,70,239,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(217,70,239,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-950\/40{--tw-gradient-from:rgba(74,4,78,.4) var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,4,78,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-indigo-500\/\[0\.1\]{--tw-gradient-from:rgba(99,102,241,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(99,102,241,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500\/25{--tw-gradient-from:rgba(236,72,153,.25) var(--tw-gradient-from-position);--tw-gradient-to:rgba(236,72,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-500\/30{--tw-gradient-from:rgba(239,68,68,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(239,68,68,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-sky-500\/\[0\.08\]{--tw-gradient-from:rgba(14,165,233,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(14,165,233,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-50{--tw-gradient-from:#f5f3ff var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,243,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/10{--tw-gradient-from:rgba(139,92,246,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/30{--tw-gradient-from:rgba(139,92,246,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/\[0\.08\]{--tw-gradient-from:rgba(139,92,246,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/\[0\.12\]{--tw-gradient-from:rgba(139,92,246,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white\/\[0\.05\]{--tw-gradient-from:hsla(0,0%,100%,.05) var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-black\/25{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(0,0,0,.25) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-white{--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#fff var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/40{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.4) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/50{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.5) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/55{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.55) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/60{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.6) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/70{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.7) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/80{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.8) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/90{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.9) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-black\/40{--tw-gradient-to:rgba(0,0,0,.4) var(--tw-gradient-to-position)}.to-blue-500{--tw-gradient-to:#3b82f6 var(--tw-gradient-to-position)}.to-cyan-400{--tw-gradient-to:#22d3ee var(--tw-gradient-to-position)}.to-cyan-50\/70{--tw-gradient-to:rgba(236,254,255,.7) var(--tw-gradient-to-position)}.to-cyan-500\/10{--tw-gradient-to:rgba(6,182,212,.1) var(--tw-gradient-to-position)}.to-cyan-500\/5,.to-cyan-500\/\[0\.05\]{--tw-gradient-to:rgba(6,182,212,.05) var(--tw-gradient-to-position)}.to-cyan-950\/20{--tw-gradient-to:rgba(8,51,68,.2) var(--tw-gradient-to-position)}.to-fuchsia-50\/80{--tw-gradient-to:rgba(253,244,255,.8) var(--tw-gradient-to-position)}.to-fuchsia-500{--tw-gradient-to:#d946ef var(--tw-gradient-to-position)}.to-fuchsia-500\/10{--tw-gradient-to:rgba(217,70,239,.1) var(--tw-gradient-to-position)}.to-fuchsia-500\/20{--tw-gradient-to:rgba(217,70,239,.2) var(--tw-gradient-to-position)}.to-fuchsia-500\/5{--tw-gradient-to:rgba(217,70,239,.05) var(--tw-gradient-to-position)}.to-fuchsia-500\/\[0\.06\]{--tw-gradient-to:rgba(217,70,239,.06) var(--tw-gradient-to-position)}.to-fuchsia-600\/25{--tw-gradient-to:rgba(192,38,211,.25) var(--tw-gradient-to-position)}.to-orange-500{--tw-gradient-to:#f97316 var(--tw-gradient-to-position)}.to-rose-500\/\[0\.05\]{--tw-gradient-to:rgba(244,63,94,.05) var(--tw-gradient-to-position)}.to-slate-50{--tw-gradient-to:#f8fafc var(--tw-gradient-to-position)}.to-teal-500{--tw-gradient-to:#14b8a6 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.to-violet-500\/10{--tw-gradient-to:rgba(139,92,246,.1) var(--tw-gradient-to-position)}.to-violet-500\/25{--tw-gradient-to:rgba(139,92,246,.25) var(--tw-gradient-to-position)}.to-violet-500\/\[0\.06\]{--tw-gradient-to:rgba(139,92,246,.06) var(--tw-gradient-to-position)}.to-violet-500\/\[0\.08\]{--tw-gradient-to:rgba(139,92,246,.08) var(--tw-gradient-to-position)}.to-violet-600\/25{--tw-gradient-to:rgba(124,58,237,.25) var(--tw-gradient-to-position)}.to-violet-600\/30{--tw-gradient-to:rgba(124,58,237,.3) var(--tw-gradient-to-position)}.to-zinc-900\/90{--tw-gradient-to:rgba(24,24,27,.9) var(--tw-gradient-to-position)}.to-zinc-950\/80{--tw-gradient-to:rgba(9,9,11,.8) var(--tw-gradient-to-position)}.object-contain{object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.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-5{padding-left:1.25rem;padding-right:1.25rem}.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-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-5{padding-left:1.25rem}.pr-1{padding-right:.25rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-\[color\:var\(--text\)\]{color:var(--text)}.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\/80{color:hsla(48,97%,77%,.8)}.text-amber-200\/85{color:hsla(48,97%,77%,.85)}.text-amber-200\/90{color:hsla(48,97%,77%,.9)}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-amber-400\/95{color:rgba(251,191,36,.95)}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-cyan-100{--tw-text-opacity:1;color:rgb(207 250 254/var(--tw-text-opacity,1))}.text-cyan-100\/90{color:rgba(207,250,254,.9)}.text-cyan-200{--tw-text-opacity:1;color:rgb(165 243 252/var(--tw-text-opacity,1))}.text-cyan-200\/90{color:rgba(165,243,252,.9)}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-300\/80{color:rgba(103,232,249,.8)}.text-cyan-300\/90{color:rgba(103,232,249,.9)}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-cyan-400\/90{color:rgba(34,211,238,.9)}.text-cyan-50{--tw-text-opacity:1;color:rgb(236 254 255/var(--tw-text-opacity,1))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity,1))}.text-cyan-800{--tw-text-opacity:1;color:rgb(21 94 117/var(--tw-text-opacity,1))}.text-cyan-950{--tw-text-opacity:1;color:rgb(8 51 68/var(--tw-text-opacity,1))}.text-emerald-100{--tw-text-opacity:1;color:rgb(209 250 229/var(--tw-text-opacity,1))}.text-emerald-100\/95{color:rgba(209,250,229,.95)}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-200\/90{color:rgba(167,243,208,.9)}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-300\/90{color:rgba(110,231,183,.9)}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/80{color:rgba(52,211,153,.8)}.text-emerald-400\/90{color:rgba(52,211,153,.9)}.text-emerald-50{--tw-text-opacity:1;color:rgb(236 253 245/var(--tw-text-opacity,1))}.text-emerald-950{--tw-text-opacity:1;color:rgb(2 44 34/var(--tw-text-opacity,1))}.text-fuchsia-100{--tw-text-opacity:1;color:rgb(250 232 255/var(--tw-text-opacity,1))}.text-fuchsia-200{--tw-text-opacity:1;color:rgb(245 208 254/var(--tw-text-opacity,1))}.text-fuchsia-200\/80{color:rgba(245,208,254,.8)}.text-fuchsia-300\/90{color:rgba(240,171,252,.9)}.text-fuchsia-400{--tw-text-opacity:1;color:rgb(232 121 249/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-300\/90{color:rgba(165,180,252,.9)}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-rose-100{--tw-text-opacity:1;color:rgb(255 228 230/var(--tw-text-opacity,1))}.text-rose-100\/95{color:rgba(255,228,230,.95)}.text-rose-200{--tw-text-opacity:1;color:rgb(254 205 211/var(--tw-text-opacity,1))}.text-rose-300{--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.text-rose-400\/90{color:rgba(251,113,133,.9)}.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-400{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-300\/90{color:rgba(203,213,225,.9)}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-violet-100{--tw-text-opacity:1;color:rgb(237 233 254/var(--tw-text-opacity,1))}.text-violet-200{--tw-text-opacity:1;color:rgb(221 214 254/var(--tw-text-opacity,1))}.text-violet-300{--tw-text-opacity:1;color:rgb(196 181 253/var(--tw-text-opacity,1))}.text-violet-300\/80{color:rgba(196,181,253,.8)}.text-violet-300\/90{color:rgba(196,181,253,.9)}.text-violet-400{--tw-text-opacity:1;color:rgb(167 139 250/var(--tw-text-opacity,1))}.text-violet-400\/90{color:rgba(167,139,250,.9)}.text-violet-600{--tw-text-opacity:1;color:rgb(124 58 237/var(--tw-text-opacity,1))}.text-violet-700{--tw-text-opacity:1;color:rgb(109 40 217/var(--tw-text-opacity,1))}.text-violet-800{--tw-text-opacity:1;color:rgb(91 33 182/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/80{color:hsla(0,0%,100%,.8)}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-violet-500{accent-color:#8b5cf6}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.opacity-\[0\.06\]{opacity:.06}.opacity-\[0\.22\]{opacity:.22}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{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 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -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-black\/20{--tw-shadow-color:rgba(0,0,0,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/30{--tw-shadow-color:rgba(0,0,0,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/80{--tw-shadow-color:rgba(0,0,0,.8);--tw-shadow:var(--tw-shadow-colored)}.shadow-cyan-900\/10{--tw-shadow-color:rgba(22,78,99,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-emerald-900\/20{--tw-shadow-color:rgba(6,78,59,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-fuchsia-100\/40{--tw-shadow-color:rgba(250,232,255,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-fuchsia-900\/10{--tw-shadow-color:rgba(112,26,117,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-indigo-500\/5{--tw-shadow-color:rgba(99,102,241,.05);--tw-shadow:var(--tw-shadow-colored)}.shadow-slate-200\/30{--tw-shadow-color:rgba(226,232,240,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-slate-200\/40{--tw-shadow-color:rgba(226,232,240,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-200\/50{--tw-shadow-color:rgba(221,214,254,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-500\/25{--tw-shadow-color:rgba(139,92,246,.25);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-500\/5{--tw-shadow-color:rgba(139,92,246,.05);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-900\/10{--tw-shadow-color:rgba(76,29,149,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-white\/5{--tw-shadow-color:hsla(0,0%,100%,.05);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.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)}.ring-2{--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(2px + 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-500\/25{--tw-ring-color:rgba(245,158,11,.25)}.ring-amber-500\/30{--tw-ring-color:rgba(245,158,11,.3)}.ring-amber-500\/35{--tw-ring-color:rgba(245,158,11,.35)}.ring-cyan-400\/35{--tw-ring-color:rgba(34,211,238,.35)}.ring-cyan-500\/20{--tw-ring-color:rgba(6,182,212,.2)}.ring-cyan-500\/25{--tw-ring-color:rgba(6,182,212,.25)}.ring-cyan-500\/30{--tw-ring-color:rgba(6,182,212,.3)}.ring-cyan-500\/35{--tw-ring-color:rgba(6,182,212,.35)}.ring-emerald-400\/25{--tw-ring-color:rgba(52,211,153,.25)}.ring-emerald-400\/30{--tw-ring-color:rgba(52,211,153,.3)}.ring-emerald-500\/20{--tw-ring-color:rgba(16,185,129,.2)}.ring-emerald-500\/25{--tw-ring-color:rgba(16,185,129,.25)}.ring-emerald-500\/30{--tw-ring-color:rgba(16,185,129,.3)}.ring-fuchsia-400\/20{--tw-ring-color:rgba(232,121,249,.2)}.ring-fuchsia-500\/35{--tw-ring-color:rgba(217,70,239,.35)}.ring-indigo-500\/25{--tw-ring-color:rgba(99,102,241,.25)}.ring-rose-500\/25{--tw-ring-color:rgba(244,63,94,.25)}.ring-rose-500\/35{--tw-ring-color:rgba(244,63,94,.35)}.ring-sky-400\/40{--tw-ring-color:rgba(56,189,248,.4)}.ring-sky-500\/25{--tw-ring-color:rgba(14,165,233,.25)}.ring-slate-500\/30{--tw-ring-color:rgba(100,116,139,.3)}.ring-violet-400\/35{--tw-ring-color:rgba(167,139,250,.35)}.ring-violet-400\/40{--tw-ring-color:rgba(167,139,250,.4)}.ring-violet-500\/20{--tw-ring-color:rgba(139,92,246,.2)}.ring-violet-500\/25{--tw-ring-color:rgba(139,92,246,.25)}.ring-violet-500\/30{--tw-ring-color:rgba(139,92,246,.3)}.ring-violet-500\/35{--tw-ring-color:rgba(139,92,246,.35)}.ring-white\/10{--tw-ring-color:hsla(0,0%,100%,.1)}.ring-white\/\[0\.04\]{--tw-ring-color:hsla(0,0%,100%,.04)}.ring-white\/\[0\.05\]{--tw-ring-color:hsla(0,0%,100%,.05)}.ring-white\/\[0\.06\]{--tw-ring-color:hsla(0,0%,100%,.06)}.ring-zinc-500\/25{--tw-ring-color:hsla(240,4%,46%,.25)}.blur-3xl{--tw-blur:blur(64px)}.blur-3xl,.blur-\[100px\]{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)}.blur-\[100px\]{--tw-blur:blur(100px)}.blur-\[120px\]{--tw-blur:blur(120px)}.blur-\[120px\],.blur-xl{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)}.blur-xl{--tw-blur:blur(24px)}.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-2xl{--tw-backdrop-blur:blur(40px)}.backdrop-blur-2xl,.backdrop-blur-\[2px\]{-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-\[2px\]{--tw-backdrop-blur:blur(2px)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-lg,.backdrop-blur-md{-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)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-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)}.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-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}.duration-300{transition-duration:.3s}.glass{border-width:1px;border-color:hsla(0,0%,100%,.08);--tw-shadow:0 25px 50px -12px rgba(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);--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored);background:var(--bg-card);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);transition:border-color .25s ease,box-shadow .25s ease,background .25s ease}html[data-theme=light] .glass{border-color:rgba(226,232,240,.9);--tw-shadow-color:rgba(148,163,184,.15);--tw-shadow:var(--tw-shadow-colored)}.section-title{font-size:1.125rem;line-height:1.75rem;font-weight:600;letter-spacing:-.025em;color:var(--text)}.section-lead{margin-top:.5rem}.meta-chip,.section-lead{font-size:.75rem;line-height:1rem;line-height:1.625;color:var(--muted)}.meta-chip{border-radius:1rem;border-width:1px;padding:.75rem 1rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;border-color:hsla(0,0%,100%,.1);background:rgba(0,0,0,.22)}html[data-theme=light] .meta-chip{border-color:rgba(226,232,240,.95);background:hsla(0,0%,100%,.72);color:var(--muted)}.tab-strip{margin-left:-.25rem;margin-right:-.25rem;display:flex;gap:.5rem;overflow-x:auto;padding-bottom:.375rem;padding-top:.125rem}@media (min-width:768px){.tab-strip{flex-wrap:wrap;overflow:visible}}.tab-strip{scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch}.tab-btn{flex-shrink:0;scroll-snap-align:start;border-radius:1rem;border-width:1px;padding:.625rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.tab-btn:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.tab-btn{--tw-ring-offset-color:var(--ring-offset);border-color:hsla(0,0%,100%,.12);background:rgba(0,0,0,.22);color:#cbd5e1}.tab-btn:hover{border-color:rgba(34,211,238,.25);color:#e2e8f0}.tab-btn-active{border-color:rgba(34,211,238,.45)!important;background:rgba(34,211,238,.12)!important;color:#cffafe!important}html[data-theme=light] .tab-btn{border-color:#e2e8f0;background:hsla(0,0%,100%,.75);color:#475569}html[data-theme=light] .tab-btn:hover{border-color:rgba(6,182,212,.45);color:#0f766e}html[data-theme=light] .tab-btn-active{border-color:rgba(6,182,212,.55)!important;background:rgba(207,250,254,.55)!important;color:#0e7490!important}.link-tile{display:block;border-radius:1rem;border-width:1px;padding:.625rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.link-tile:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.link-tile{--tw-ring-offset-color:var(--ring-offset);border-color:hsla(0,0%,100%,.1);background:rgba(0,0,0,.2);color:#f1f5f9}.link-tile:hover{border-color:rgba(34,211,238,.35)}html[data-theme=light] .link-tile{border-color:#e2e8f0;background:hsla(0,0%,100%,.65);color:#1e293b}.link-tile-accent{border-color:rgba(34,211,238,.28);background:rgba(34,211,238,.1);color:#ecfeff}.link-tile-accent:hover{border-color:rgba(34,211,238,.55)}html[data-theme=light] .link-tile-accent{border-color:rgba(6,182,212,.35);background:rgba(207,250,254,.65);color:#0e7490}.link-tile-warn{border-color:rgba(251,191,36,.25);background:rgba(251,191,36,.1);color:#fefce8}html[data-theme=light] .link-tile-warn{border-color:rgba(251,191,36,.45);background:hsla(55,92%,95%,.9);color:#78350f}.service-card{border-radius:1.5rem;border-width:1px;padding:1.25rem;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s;border-color:hsla(0,0%,100%,.08);background:rgba(0,0,0,.2)}.service-card:hover{box-shadow:0 12px 40px -24px rgba(34,211,238,.35)}html[data-theme=light] .service-card{border-color:rgba(226,232,240,.95);background:hsla(0,0%,100%,.55)}.panel-inset{border-radius:1rem;border-width:1px;padding:1rem;font-size:.875rem;line-height:1.25rem;border-color:hsla(0,0%,100%,.08);background:rgba(0,0,0,.2);color:#cbd5e1}html[data-theme=light] .panel-inset{border-color:rgba(226,232,240,.85);background:hsla(0,0%,100%,.5);color:#334155}.field-select{width:100%;border-radius:1rem;border-width:1px;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem;outline:2px solid transparent;outline-offset:2px;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;border-color:hsla(0,0%,100%,.12);background:rgba(2,6,23,.55);color:#f8fafc}html[data-theme=light] .field-select{border-color:#e2e8f0;background:hsla(0,0%,100%,.9);color:#0f172a}.btn-focus:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.btn-focus{--tw-ring-offset-color:var(--ring-offset)}.\[-ms-overflow-style\:none\]{-ms-overflow-style:none}.\[scrollbar-width\:none\]{scrollbar-width:none}:root{--bg-deep:#060912;--bg-card:rgba(16,22,36,.78);--accent:#22d3ee;--accent2:#a78bfa;--danger:#f43f5e;--ok:#34d399;--text:#e8edf7;--muted:#94a3b8;--ring-offset:#060912;color-scheme:dark}html[data-theme=light]{--bg-deep:#f1f5f9;--bg-card:hsla(0,0%,100%,.88);--text:#0f172a;--muted:#64748b;--ring-offset:#f1f5f9;color-scheme:light}html{scroll-behavior:smooth}body{color:var(--text);background:var(--bg-deep);min-height:100vh;font-family:var(--font-geist-sans),system-ui,sans-serif;overflow-x:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:background-color .35s ease,color .25s ease}*{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.35) transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:rgba(148,163,184,.35);border-radius:999px}::-webkit-scrollbar-track{background:transparent}.app-bg-layer{position:fixed;inset:0;z-index:-1;opacity:.92;pointer-events:none;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(34,211,238,.22),transparent),radial-gradient(ellipse 60% 40% at 100% 0,rgba(167,139,250,.18),transparent),radial-gradient(ellipse 50% 30% at 0 100%,rgba(59,130,246,.12),transparent);transition:opacity .35s ease}html[data-theme=light] .app-bg-layer{opacity:.75;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(14,165,233,.12),transparent),radial-gradient(ellipse 60% 40% at 100% 0,rgba(139,92,246,.1),transparent),radial-gradient(ellipse 50% 30% at 0 100%,rgba(59,130,246,.08),transparent)}html[data-theme=light] .chart-surface{border-color:rgba(15,23,42,.12)!important;background:hsla(0,0%,100%,.65)!important}::selection{background:rgba(34,211,238,.25)}.safe-pb-nav{padding-bottom:calc(5.5rem + env(safe-area-inset-bottom, 0px))}@media (min-width:768px){.safe-pb-nav{padding-bottom:4rem}}@keyframes toast-in{0%{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%)}}.animate-toast-in{animation:toast-in .35s ease-out both}@keyframes pulse-soft{0%,to{opacity:1}50%{opacity:.65}}.animate-pulse-soft{animation:pulse-soft 2s ease-in-out infinite}.log-scroll{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.35) transparent}.log-scroll::-webkit-scrollbar{width:6px;height:6px}.log-scroll::-webkit-scrollbar-thumb{background:rgba(148,163,184,.35);border-radius:6px}.portal-grid-bg{background-image:linear-gradient(hsla(0,0%,100%,.05) 1px,transparent 0),linear-gradient(90deg,hsla(0,0%,100%,.05) 1px,transparent 0);background-size:28px 28px}.lp-noise{background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E")}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:after,:before{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}.selection\:bg-violet-400\/25 ::selection{background-color:rgba(167,139,250,.25)}.selection\:bg-violet-500\/30 ::selection{background-color:rgba(139,92,246,.3)}.selection\:bg-violet-400\/25::selection{background-color:rgba(167,139,250,.25)}.selection\:bg-violet-500\/30::selection{background-color:rgba(139,92,246,.3)}.last\:border-0:last-child{border-width:0}.hover\:border-violet-400\/25:hover{border-color:rgba(167,139,250,.25)}.hover\:border-white\/15:hover{border-color:hsla(0,0%,100%,.15)}.hover\:border-white\/20:hover{border-color:hsla(0,0%,100%,.2)}.hover\:bg-black\/45:hover{background-color:rgba(0,0,0,.45)}.hover\:bg-cyan-500\/15:hover{background-color:rgba(6,182,212,.15)}.hover\:bg-cyan-500\/25:hover{background-color:rgba(6,182,212,.25)}.hover\:bg-slate-100:hover{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.hover\:bg-slate-200\/60:hover{background-color:rgba(226,232,240,.6)}.hover\:bg-slate-50:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.hover\:bg-violet-500\/25:hover{background-color:rgba(139,92,246,.25)}.hover\:bg-violet-500\/30:hover{background-color:rgba(139,92,246,.3)}.hover\:bg-white\/10:hover{background-color:hsla(0,0%,100%,.1)}.hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.hover\:bg-white\/\[0\.04\]:hover{background-color:hsla(0,0%,100%,.04)}.hover\:bg-white\/\[0\.07\]:hover{background-color:hsla(0,0%,100%,.07)}.hover\:bg-white\/\[0\.08\]:hover{background-color:hsla(0,0%,100%,.08)}.hover\:bg-white\/\[0\.1\]:hover{background-color:hsla(0,0%,100%,.1)}.hover\:text-amber-300:hover{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.hover\:text-cyan-200:hover{--tw-text-opacity:1;color:rgb(165 243 252/var(--tw-text-opacity,1))}.hover\:text-rose-200:hover{--tw-text-opacity:1;color:rgb(254 205 211/var(--tw-text-opacity,1))}.hover\:text-slate-900:hover{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.hover\:text-zinc-200:hover{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.hover\:text-zinc-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-violet-400\/50:hover{--tw-ring-color:rgba(167,139,250,.5)}.hover\:brightness-110:hover{--tw-brightness:brightness(1.1);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)}.focus-visible\:ring-2:focus-visible{--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(2px + 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)}.focus-visible\:ring-emerald-500\/50:focus-visible{--tw-ring-color:rgba(16,185,129,.5)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}@media (min-width:640px){.sm\:block{display:block}.sm\:inline{display:inline}.sm\:w-auto{width:auto}.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\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:flex-nowrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-2{gap:.5rem}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (min-width:768px){.md\:bottom-6{bottom:1.5rem}.md\:hidden{display:none}.md\:h-72{height:18rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-start{align-items:flex-start}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-8{padding-top:2rem;padding-bottom:2rem}.lg\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-8{grid-column:span 8/span 8}.xl\:max-h-\[min\(24rem\2c 50vh\)\]{max-height:min(24rem,50vh)}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-\[0\.95fr\2c 1\.05fr\]{grid-template-columns:.95fr 1.05fr}.xl\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.xl\:grid-cols-\[18rem\2c 1fr\]{grid-template-columns:18rem 1fr}}.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar{display:none}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:border-cyan-600\/25{border-color:rgba(8,145,178,.25)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:border-slate-200\/60{border-color:rgba(226,232,240,.6)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:bg-white\/90{background-color:hsla(0,0%,100%,.9)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:bg-white\/95{background-color:hsla(0,0%,100%,.95)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-cyan-700{--tw-text-opacity:1;color:rgb(14 116 144/var(--tw-text-opacity,1))}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-cyan-950{--tw-text-opacity:1;color:rgb(8 51 68/var(--tw-text-opacity,1))}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))} \ No newline at end of file +*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-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]){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,pre,samp{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,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{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}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-bottom-0\.5{bottom:-.125rem}.-left-1\/4{left:-25%}.-right-0\.5{right:-.125rem}.-right-20{right:-5rem}.-right-4{right:-1rem}.-top-20{top:-5rem}.-top-4{top:-1rem}.bottom-0{bottom:0}.bottom-24{bottom:6rem}.bottom-3{bottom:.75rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[100\]{z-index:100}.col-span-2{grid-column:span 2/span 2}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.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}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-2,.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-6{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:6}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-11{height:2.75rem}.h-12{height:3rem}.h-16{height:4rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-36{height:9rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-56{height:14rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-9{height:2.25rem}.h-\[30rem\]{height:30rem}.h-\[36rem\]{height:36rem}.h-\[42rem\]{height:42rem}.h-\[min\(72vh\2c 40rem\)\]{height:min(72vh,40rem)}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-72{max-height:18rem}.max-h-\[28rem\]{max-height:28rem}.max-h-\[min\(22rem\2c 42vh\)\]{max-height:min(22rem,42vh)}.max-h-\[min\(24rem\2c 55vh\)\]{max-height:min(24rem,55vh)}.max-h-\[min\(70vh\2c 48rem\)\]{max-height:min(70vh,48rem)}.max-h-\[min\(72vh\2c 680px\)\]{max-height:min(72vh,680px)}.max-h-\[min\(78vh\2c 56rem\)\]{max-height:min(78vh,56rem)}.max-h-\[min\(78vh\2c 720px\)\]{max-height:min(78vh,720px)}.min-h-0{min-height:0}.min-h-\[100px\]{min-height:100px}.min-h-\[12rem\]{min-height:12rem}.min-h-\[14rem\]{min-height:14rem}.min-h-\[160px\]{min-height:160px}.min-h-\[18rem\]{min-height:18rem}.min-h-\[2\.75rem\]{min-height:2.75rem}.min-h-\[200px\]{min-height:200px}.min-h-\[22rem\]{min-height:22rem}.min-h-\[280px\]{min-height:280px}.min-h-\[2rem\]{min-height:2rem}.min-h-\[32rem\]{min-height:32rem}.min-h-\[3rem\]{min-height:3rem}.min-h-\[4rem\]{min-height:4rem}.min-h-\[6rem\]{min-height:6rem}.min-h-\[72px\]{min-height:72px}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-12{width:3rem}.w-16{width:4rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-9{width:2.25rem}.w-\[36rem\]{width:36rem}.w-\[42rem\]{width:42rem}.w-\[min\(88vw\2c 280px\)\]{width:min(88vw,280px)}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[12rem\]{min-width:12rem}.min-w-\[1rem\]{min-width:1rem}.min-w-\[6rem\]{min-width:6rem}.min-w-\[7rem\]{min-width:7rem}.min-w-\[9rem\]{min-width:9rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-\[min\(92vw\2c 28rem\)\]{max-width:min(92vw,28rem)}.max-w-full{max-width:100%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.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-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.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{column-gap:.5rem}.gap-y-0\.5{row-gap:.125rem}.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-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.625rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.625rem * 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-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * 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}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[1\.35rem\]{border-radius:1.35rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-xl{border-bottom-right-radius:.75rem;border-bottom-left-radius:.75rem}.border{border-width:1px}.border-\[10px\]{border-width:10px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-400\/50{border-color:rgba(251,191,36,.5)}.border-amber-500\/15{border-color:rgba(245,158,11,.15)}.border-amber-500\/20{border-color:rgba(245,158,11,.2)}.border-amber-500\/25{border-color:rgba(245,158,11,.25)}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-cyan-400\/35{border-color:rgba(34,211,238,.35)}.border-cyan-400\/50{border-color:rgba(34,211,238,.5)}.border-cyan-500\/15{border-color:rgba(6,182,212,.15)}.border-cyan-500\/20{border-color:rgba(6,182,212,.2)}.border-cyan-500\/25{border-color:rgba(6,182,212,.25)}.border-cyan-500\/30{border-color:rgba(6,182,212,.3)}.border-cyan-500\/40{border-color:rgba(6,182,212,.4)}.border-cyan-500\/50{border-color:rgba(6,182,212,.5)}.border-emerald-400\/30{border-color:rgba(52,211,153,.3)}.border-emerald-500\/15{border-color:rgba(16,185,129,.15)}.border-emerald-500\/20{border-color:rgba(16,185,129,.2)}.border-emerald-500\/25{border-color:rgba(16,185,129,.25)}.border-emerald-500\/30{border-color:rgba(16,185,129,.3)}.border-emerald-500\/50{border-color:rgba(16,185,129,.5)}.border-fuchsia-200\/80{border-color:rgba(245,208,254,.8)}.border-fuchsia-400\/20{border-color:rgba(232,121,249,.2)}.border-fuchsia-500\/20{border-color:rgba(217,70,239,.2)}.border-fuchsia-500\/25{border-color:rgba(217,70,239,.25)}.border-fuchsia-500\/30{border-color:rgba(217,70,239,.3)}.border-indigo-400\/50{border-color:rgba(129,140,248,.5)}.border-indigo-500\/25{border-color:rgba(99,102,241,.25)}.border-rose-500\/15{border-color:rgba(244,63,94,.15)}.border-rose-500\/20{border-color:rgba(244,63,94,.2)}.border-rose-500\/25{border-color:rgba(244,63,94,.25)}.border-rose-500\/30{border-color:rgba(244,63,94,.3)}.border-sky-500\/20{border-color:rgba(14,165,233,.2)}.border-slate-200{--tw-border-opacity:1;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-200\/80{border-color:rgba(226,232,240,.8)}.border-slate-200\/90{border-color:rgba(226,232,240,.9)}.border-slate-500\/30{border-color:rgba(100,116,139,.3)}.border-slate-700\/30{border-color:rgba(51,65,85,.3)}.border-violet-200\/90{border-color:rgba(221,214,254,.9)}.border-violet-400\/35{border-color:rgba(167,139,250,.35)}.border-violet-500\/15{border-color:rgba(139,92,246,.15)}.border-violet-500\/20{border-color:rgba(139,92,246,.2)}.border-violet-500\/25{border-color:rgba(139,92,246,.25)}.border-violet-500\/30{border-color:rgba(139,92,246,.3)}.border-violet-500\/35{border-color:rgba(139,92,246,.35)}.border-violet-500\/45{border-color:rgba(139,92,246,.45)}.border-violet-500\/50{border-color:rgba(139,92,246,.5)}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.border-white\/15{border-color:hsla(0,0%,100%,.15)}.border-white\/20{border-color:hsla(0,0%,100%,.2)}.border-white\/5{border-color:hsla(0,0%,100%,.05)}.border-white\/\[0\.04\]{border-color:hsla(0,0%,100%,.04)}.border-white\/\[0\.05\]{border-color:hsla(0,0%,100%,.05)}.border-white\/\[0\.06\]{border-color:hsla(0,0%,100%,.06)}.border-white\/\[0\.07\]{border-color:hsla(0,0%,100%,.07)}.border-white\/\[0\.08\]{border-color:hsla(0,0%,100%,.08)}.border-zinc-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.bg-\[\#050508\]{--tw-bg-opacity:1;background-color:rgb(5 5 8/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:rgba(245,158,11,.1)}.bg-amber-500\/15{background-color:rgba(245,158,11,.15)}.bg-amber-500\/20{background-color:rgba(245,158,11,.2)}.bg-amber-500\/5{background-color:rgba(245,158,11,.05)}.bg-amber-500\/\[0\.06\]{background-color:rgba(245,158,11,.06)}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.bg-black\/15{background-color:rgba(0,0,0,.15)}.bg-black\/20{background-color:rgba(0,0,0,.2)}.bg-black\/25{background-color:rgba(0,0,0,.25)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/35{background-color:rgba(0,0,0,.35)}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-black\/45{background-color:rgba(0,0,0,.45)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/55{background-color:rgba(0,0,0,.55)}.bg-black\/60{background-color:rgba(0,0,0,.6)}.bg-black\/75{background-color:rgba(0,0,0,.75)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-cyan-100{--tw-bg-opacity:1;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.bg-cyan-400\/10{background-color:rgba(34,211,238,.1)}.bg-cyan-500\/10{background-color:rgba(6,182,212,.1)}.bg-cyan-500\/15{background-color:rgba(6,182,212,.15)}.bg-cyan-500\/20{background-color:rgba(6,182,212,.2)}.bg-cyan-500\/30{background-color:rgba(6,182,212,.3)}.bg-cyan-500\/90{background-color:rgba(6,182,212,.9)}.bg-cyan-500\/\[0\.05\]{background-color:rgba(6,182,212,.05)}.bg-cyan-500\/\[0\.06\]{background-color:rgba(6,182,212,.06)}.bg-cyan-600{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-cyan-600\/80{background-color:rgba(8,145,178,.8)}.bg-emerald-500\/10{background-color:rgba(16,185,129,.1)}.bg-emerald-500\/15{background-color:rgba(16,185,129,.15)}.bg-emerald-500\/20{background-color:rgba(16,185,129,.2)}.bg-emerald-500\/90{background-color:rgba(16,185,129,.9)}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-fuchsia-500\/10{background-color:rgba(217,70,239,.1)}.bg-fuchsia-500\/20{background-color:rgba(217,70,239,.2)}.bg-indigo-500\/15{background-color:rgba(99,102,241,.15)}.bg-rose-500\/10{background-color:rgba(244,63,94,.1)}.bg-rose-500\/15{background-color:rgba(244,63,94,.15)}.bg-rose-600{--tw-bg-opacity:1;background-color:rgb(225 29 72/var(--tw-bg-opacity,1))}.bg-rose-950\/20{background-color:rgba(76,5,25,.2)}.bg-rose-950\/25{background-color:rgba(76,5,25,.25)}.bg-sky-500\/15{background-color:rgba(14,165,233,.15)}.bg-sky-500\/25{background-color:rgba(14,165,233,.25)}.bg-slate-100{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-slate-500\/10{background-color:rgba(100,116,139,.1)}.bg-slate-500\/15{background-color:rgba(100,116,139,.15)}.bg-slate-800\/50{background-color:rgba(30,41,59,.5)}.bg-slate-950\/30{background-color:rgba(2,6,23,.3)}.bg-slate-950\/50{background-color:rgba(2,6,23,.5)}.bg-slate-950\/60{background-color:rgba(2,6,23,.6)}.bg-slate-950\/80{background-color:rgba(2,6,23,.8)}.bg-slate-950\/95{background-color:rgba(2,6,23,.95)}.bg-violet-100{--tw-bg-opacity:1;background-color:rgb(237 233 254/var(--tw-bg-opacity,1))}.bg-violet-100\/90{background-color:rgba(237,233,254,.9)}.bg-violet-400\/15{background-color:rgba(167,139,250,.15)}.bg-violet-500\/10{background-color:rgba(139,92,246,.1)}.bg-violet-500\/15{background-color:rgba(139,92,246,.15)}.bg-violet-500\/20{background-color:rgba(139,92,246,.2)}.bg-violet-500\/25{background-color:rgba(139,92,246,.25)}.bg-violet-500\/\[0\.06\]{background-color:rgba(139,92,246,.06)}.bg-violet-500\/\[0\.1\]{background-color:rgba(139,92,246,.1)}.bg-violet-600{--tw-bg-opacity:1;background-color:rgb(124 58 237/var(--tw-bg-opacity,1))}.bg-violet-600\/20{background-color:rgba(124,58,237,.2)}.bg-violet-600\/70{background-color:rgba(124,58,237,.7)}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/10{background-color:hsla(0,0%,100%,.1)}.bg-white\/5{background-color:hsla(0,0%,100%,.05)}.bg-white\/70{background-color:hsla(0,0%,100%,.7)}.bg-white\/75{background-color:hsla(0,0%,100%,.75)}.bg-white\/80{background-color:hsla(0,0%,100%,.8)}.bg-white\/90{background-color:hsla(0,0%,100%,.9)}.bg-white\/\[0\.04\]{background-color:hsla(0,0%,100%,.04)}.bg-white\/\[0\.05\]{background-color:hsla(0,0%,100%,.05)}.bg-white\/\[0\.06\]{background-color:hsla(0,0%,100%,.06)}.bg-white\/\[0\.08\]{background-color:hsla(0,0%,100%,.08)}.bg-zinc-500\/15{background-color:hsla(240,4%,46%,.15)}.bg-zinc-600\/20{background-color:rgba(82,82,91,.2)}.bg-zinc-600\/30{background-color:rgba(82,82,91,.3)}.bg-zinc-700\/40{background-color:rgba(63,63,70,.4)}.bg-zinc-700\/90{background-color:rgba(63,63,70,.9)}.bg-zinc-950{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1))}.bg-zinc-950\/30{background-color:rgba(9,9,11,.3)}.bg-zinc-950\/40{background-color:rgba(9,9,11,.4)}.bg-zinc-950\/50{background-color:rgba(9,9,11,.5)}.bg-zinc-950\/60{background-color:rgba(9,9,11,.6)}.bg-zinc-950\/70{background-color:rgba(9,9,11,.7)}.bg-zinc-950\/80{background-color:rgba(9,9,11,.8)}.bg-zinc-950\/95{background-color:rgba(9,9,11,.95)}.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-400{--tw-gradient-from:#fbbf24 var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,191,36,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-50\/90{--tw-gradient-from:rgba(255,251,235,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(255,251,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/35{--tw-gradient-from:rgba(245,158,11,.35) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.07\]{--tw-gradient-from:rgba(245,158,11,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.12\]{--tw-gradient-from:rgba(245,158,11,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-amber-500\/\[0\.1\]{--tw-gradient-from:rgba(245,158,11,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,158,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-400{--tw-gradient-from:#22d3ee var(--tw-gradient-from-position);--tw-gradient-to:rgba(34,211,238,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/35{--tw-gradient-from:rgba(6,182,212,.35) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.07\]{--tw-gradient-from:rgba(6,182,212,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.08\]{--tw-gradient-from:rgba(6,182,212,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.14\]{--tw-gradient-from:rgba(6,182,212,.14) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-cyan-500\/\[0\.1\]{--tw-gradient-from:rgba(6,182,212,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(6,182,212,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-400{--tw-gradient-from:#34d399 var(--tw-gradient-from-position);--tw-gradient-to:rgba(52,211,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-400\/30{--tw-gradient-from:rgba(52,211,153,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(52,211,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.07\]{--tw-gradient-from:rgba(16,185,129,.07) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.08\]{--tw-gradient-from:rgba(16,185,129,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-emerald-500\/\[0\.12\]{--tw-gradient-from:rgba(16,185,129,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(16,185,129,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-50\/90{--tw-gradient-from:rgba(253,244,255,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(253,244,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-500\/10{--tw-gradient-from:rgba(217,70,239,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(217,70,239,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-500\/\[0\.08\]{--tw-gradient-from:rgba(217,70,239,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(217,70,239,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-fuchsia-950\/40{--tw-gradient-from:rgba(74,4,78,.4) var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,4,78,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-indigo-500\/\[0\.1\]{--tw-gradient-from:rgba(99,102,241,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(99,102,241,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-pink-500\/25{--tw-gradient-from:rgba(236,72,153,.25) var(--tw-gradient-from-position);--tw-gradient-to:rgba(236,72,153,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-500\/30{--tw-gradient-from:rgba(239,68,68,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(239,68,68,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-sky-500\/\[0\.08\]{--tw-gradient-from:rgba(14,165,233,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(14,165,233,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-50{--tw-gradient-from:#f5f3ff var(--tw-gradient-from-position);--tw-gradient-to:rgba(245,243,255,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500{--tw-gradient-from:#8b5cf6 var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/10{--tw-gradient-from:rgba(139,92,246,.1) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/30{--tw-gradient-from:rgba(139,92,246,.3) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/\[0\.08\]{--tw-gradient-from:rgba(139,92,246,.08) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-violet-500\/\[0\.12\]{--tw-gradient-from:rgba(139,92,246,.12) var(--tw-gradient-from-position);--tw-gradient-to:rgba(139,92,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-white\/\[0\.05\]{--tw-gradient-from:hsla(0,0%,100%,.05) var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-black\/25{--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(0,0,0,.25) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-white{--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#fff var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/40{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.4) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/50{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.5) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/55{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.55) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/60{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.6) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/70{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.7) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/80{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.8) var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-zinc-950\/90{--tw-gradient-to:rgba(9,9,11,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),rgba(9,9,11,.9) var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-black\/40{--tw-gradient-to:rgba(0,0,0,.4) var(--tw-gradient-to-position)}.to-blue-500{--tw-gradient-to:#3b82f6 var(--tw-gradient-to-position)}.to-cyan-400{--tw-gradient-to:#22d3ee var(--tw-gradient-to-position)}.to-cyan-50\/70{--tw-gradient-to:rgba(236,254,255,.7) var(--tw-gradient-to-position)}.to-cyan-500\/10{--tw-gradient-to:rgba(6,182,212,.1) var(--tw-gradient-to-position)}.to-cyan-500\/5,.to-cyan-500\/\[0\.05\]{--tw-gradient-to:rgba(6,182,212,.05) var(--tw-gradient-to-position)}.to-cyan-950\/20{--tw-gradient-to:rgba(8,51,68,.2) var(--tw-gradient-to-position)}.to-fuchsia-50\/80{--tw-gradient-to:rgba(253,244,255,.8) var(--tw-gradient-to-position)}.to-fuchsia-500{--tw-gradient-to:#d946ef var(--tw-gradient-to-position)}.to-fuchsia-500\/10{--tw-gradient-to:rgba(217,70,239,.1) var(--tw-gradient-to-position)}.to-fuchsia-500\/20{--tw-gradient-to:rgba(217,70,239,.2) var(--tw-gradient-to-position)}.to-fuchsia-500\/5{--tw-gradient-to:rgba(217,70,239,.05) var(--tw-gradient-to-position)}.to-fuchsia-500\/\[0\.06\]{--tw-gradient-to:rgba(217,70,239,.06) var(--tw-gradient-to-position)}.to-fuchsia-600\/25{--tw-gradient-to:rgba(192,38,211,.25) var(--tw-gradient-to-position)}.to-orange-500{--tw-gradient-to:#f97316 var(--tw-gradient-to-position)}.to-rose-500\/\[0\.05\]{--tw-gradient-to:rgba(244,63,94,.05) var(--tw-gradient-to-position)}.to-slate-50{--tw-gradient-to:#f8fafc var(--tw-gradient-to-position)}.to-teal-500{--tw-gradient-to:#14b8a6 var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.to-violet-500\/10{--tw-gradient-to:rgba(139,92,246,.1) var(--tw-gradient-to-position)}.to-violet-500\/25{--tw-gradient-to:rgba(139,92,246,.25) var(--tw-gradient-to-position)}.to-violet-500\/\[0\.06\]{--tw-gradient-to:rgba(139,92,246,.06) var(--tw-gradient-to-position)}.to-violet-500\/\[0\.08\]{--tw-gradient-to:rgba(139,92,246,.08) var(--tw-gradient-to-position)}.to-violet-600\/25{--tw-gradient-to:rgba(124,58,237,.25) var(--tw-gradient-to-position)}.to-violet-600\/30{--tw-gradient-to:rgba(124,58,237,.3) var(--tw-gradient-to-position)}.to-zinc-900\/90{--tw-gradient-to:rgba(24,24,27,.9) var(--tw-gradient-to-position)}.to-zinc-950\/80{--tw-gradient-to:rgba(9,9,11,.8) var(--tw-gradient-to-position)}.object-contain{object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.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-5{padding-left:1.25rem;padding-right:1.25rem}.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-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-24{padding-top:6rem;padding-bottom:6rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-16{padding-bottom:4rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-5{padding-left:1.25rem}.pr-1{padding-right:.25rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-\[color\:var\(--text\)\]{color:var(--text)}.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\/80{color:hsla(48,97%,77%,.8)}.text-amber-200\/85{color:hsla(48,97%,77%,.85)}.text-amber-200\/90{color:hsla(48,97%,77%,.9)}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-amber-400\/95{color:rgba(251,191,36,.95)}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-amber-800{--tw-text-opacity:1;color:rgb(146 64 14/var(--tw-text-opacity,1))}.text-cyan-100{--tw-text-opacity:1;color:rgb(207 250 254/var(--tw-text-opacity,1))}.text-cyan-100\/90{color:rgba(207,250,254,.9)}.text-cyan-200{--tw-text-opacity:1;color:rgb(165 243 252/var(--tw-text-opacity,1))}.text-cyan-200\/90{color:rgba(165,243,252,.9)}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-300\/70{color:rgba(103,232,249,.7)}.text-cyan-300\/80{color:rgba(103,232,249,.8)}.text-cyan-300\/90{color:rgba(103,232,249,.9)}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-cyan-400\/90{color:rgba(34,211,238,.9)}.text-cyan-50{--tw-text-opacity:1;color:rgb(236 254 255/var(--tw-text-opacity,1))}.text-cyan-600{--tw-text-opacity:1;color:rgb(8 145 178/var(--tw-text-opacity,1))}.text-cyan-800{--tw-text-opacity:1;color:rgb(21 94 117/var(--tw-text-opacity,1))}.text-cyan-950{--tw-text-opacity:1;color:rgb(8 51 68/var(--tw-text-opacity,1))}.text-emerald-100{--tw-text-opacity:1;color:rgb(209 250 229/var(--tw-text-opacity,1))}.text-emerald-100\/95{color:rgba(209,250,229,.95)}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-200\/90{color:rgba(167,243,208,.9)}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-emerald-300\/90{color:rgba(110,231,183,.9)}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/80{color:rgba(52,211,153,.8)}.text-emerald-400\/90{color:rgba(52,211,153,.9)}.text-emerald-50{--tw-text-opacity:1;color:rgb(236 253 245/var(--tw-text-opacity,1))}.text-emerald-950{--tw-text-opacity:1;color:rgb(2 44 34/var(--tw-text-opacity,1))}.text-fuchsia-100{--tw-text-opacity:1;color:rgb(250 232 255/var(--tw-text-opacity,1))}.text-fuchsia-200{--tw-text-opacity:1;color:rgb(245 208 254/var(--tw-text-opacity,1))}.text-fuchsia-200\/80{color:rgba(245,208,254,.8)}.text-fuchsia-300\/90{color:rgba(240,171,252,.9)}.text-fuchsia-400{--tw-text-opacity:1;color:rgb(232 121 249/var(--tw-text-opacity,1))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity,1))}.text-indigo-300\/90{color:rgba(165,180,252,.9)}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-rose-100{--tw-text-opacity:1;color:rgb(255 228 230/var(--tw-text-opacity,1))}.text-rose-100\/95{color:rgba(255,228,230,.95)}.text-rose-200{--tw-text-opacity:1;color:rgb(254 205 211/var(--tw-text-opacity,1))}.text-rose-300{--tw-text-opacity:1;color:rgb(253 164 175/var(--tw-text-opacity,1))}.text-rose-400\/90{color:rgba(251,113,133,.9)}.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-400{--tw-text-opacity:1;color:rgb(56 189 248/var(--tw-text-opacity,1))}.text-slate-100{--tw-text-opacity:1;color:rgb(241 245 249/var(--tw-text-opacity,1))}.text-slate-200{--tw-text-opacity:1;color:rgb(226 232 240/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-300\/90{color:rgba(203,213,225,.9)}.text-slate-400{--tw-text-opacity:1;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-slate-500{--tw-text-opacity:1;color:rgb(100 116 139/var(--tw-text-opacity,1))}.text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-slate-700{--tw-text-opacity:1;color:rgb(51 65 85/var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity,1))}.text-slate-900{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-violet-100{--tw-text-opacity:1;color:rgb(237 233 254/var(--tw-text-opacity,1))}.text-violet-200{--tw-text-opacity:1;color:rgb(221 214 254/var(--tw-text-opacity,1))}.text-violet-300{--tw-text-opacity:1;color:rgb(196 181 253/var(--tw-text-opacity,1))}.text-violet-300\/80{color:rgba(196,181,253,.8)}.text-violet-300\/90{color:rgba(196,181,253,.9)}.text-violet-400{--tw-text-opacity:1;color:rgb(167 139 250/var(--tw-text-opacity,1))}.text-violet-400\/90{color:rgba(167,139,250,.9)}.text-violet-600{--tw-text-opacity:1;color:rgb(124 58 237/var(--tw-text-opacity,1))}.text-violet-700{--tw-text-opacity:1;color:rgb(109 40 217/var(--tw-text-opacity,1))}.text-violet-800{--tw-text-opacity:1;color:rgb(91 33 182/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/80{color:hsla(0,0%,100%,.8)}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-violet-500{accent-color:#8b5cf6}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.opacity-\[0\.06\]{opacity:.06}.opacity-\[0\.22\]{opacity:.22}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-inner{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-sm{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 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -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-black\/20{--tw-shadow-color:rgba(0,0,0,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/30{--tw-shadow-color:rgba(0,0,0,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/50{--tw-shadow-color:rgba(0,0,0,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-black\/80{--tw-shadow-color:rgba(0,0,0,.8);--tw-shadow:var(--tw-shadow-colored)}.shadow-cyan-900\/10{--tw-shadow-color:rgba(22,78,99,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-emerald-900\/20{--tw-shadow-color:rgba(6,78,59,.2);--tw-shadow:var(--tw-shadow-colored)}.shadow-fuchsia-100\/40{--tw-shadow-color:rgba(250,232,255,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-fuchsia-900\/10{--tw-shadow-color:rgba(112,26,117,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-indigo-500\/5{--tw-shadow-color:rgba(99,102,241,.05);--tw-shadow:var(--tw-shadow-colored)}.shadow-slate-200\/30{--tw-shadow-color:rgba(226,232,240,.3);--tw-shadow:var(--tw-shadow-colored)}.shadow-slate-200\/40{--tw-shadow-color:rgba(226,232,240,.4);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-200\/50{--tw-shadow-color:rgba(221,214,254,.5);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-500\/25{--tw-shadow-color:rgba(139,92,246,.25);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-500\/5{--tw-shadow-color:rgba(139,92,246,.05);--tw-shadow:var(--tw-shadow-colored)}.shadow-violet-900\/10{--tw-shadow-color:rgba(76,29,149,.1);--tw-shadow:var(--tw-shadow-colored)}.shadow-white\/5{--tw-shadow-color:hsla(0,0%,100%,.05);--tw-shadow:var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.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)}.ring-2{--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(2px + 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-500\/25{--tw-ring-color:rgba(245,158,11,.25)}.ring-amber-500\/30{--tw-ring-color:rgba(245,158,11,.3)}.ring-amber-500\/35{--tw-ring-color:rgba(245,158,11,.35)}.ring-cyan-400\/35{--tw-ring-color:rgba(34,211,238,.35)}.ring-cyan-500\/20{--tw-ring-color:rgba(6,182,212,.2)}.ring-cyan-500\/25{--tw-ring-color:rgba(6,182,212,.25)}.ring-cyan-500\/30{--tw-ring-color:rgba(6,182,212,.3)}.ring-cyan-500\/35{--tw-ring-color:rgba(6,182,212,.35)}.ring-emerald-400\/25{--tw-ring-color:rgba(52,211,153,.25)}.ring-emerald-400\/30{--tw-ring-color:rgba(52,211,153,.3)}.ring-emerald-500\/20{--tw-ring-color:rgba(16,185,129,.2)}.ring-emerald-500\/25{--tw-ring-color:rgba(16,185,129,.25)}.ring-emerald-500\/30{--tw-ring-color:rgba(16,185,129,.3)}.ring-fuchsia-400\/20{--tw-ring-color:rgba(232,121,249,.2)}.ring-fuchsia-500\/35{--tw-ring-color:rgba(217,70,239,.35)}.ring-indigo-500\/25{--tw-ring-color:rgba(99,102,241,.25)}.ring-rose-500\/25{--tw-ring-color:rgba(244,63,94,.25)}.ring-rose-500\/35{--tw-ring-color:rgba(244,63,94,.35)}.ring-sky-400\/40{--tw-ring-color:rgba(56,189,248,.4)}.ring-sky-500\/25{--tw-ring-color:rgba(14,165,233,.25)}.ring-slate-500\/30{--tw-ring-color:rgba(100,116,139,.3)}.ring-violet-400\/35{--tw-ring-color:rgba(167,139,250,.35)}.ring-violet-400\/40{--tw-ring-color:rgba(167,139,250,.4)}.ring-violet-500\/20{--tw-ring-color:rgba(139,92,246,.2)}.ring-violet-500\/25{--tw-ring-color:rgba(139,92,246,.25)}.ring-violet-500\/30{--tw-ring-color:rgba(139,92,246,.3)}.ring-violet-500\/35{--tw-ring-color:rgba(139,92,246,.35)}.ring-white\/10{--tw-ring-color:hsla(0,0%,100%,.1)}.ring-white\/\[0\.04\]{--tw-ring-color:hsla(0,0%,100%,.04)}.ring-white\/\[0\.05\]{--tw-ring-color:hsla(0,0%,100%,.05)}.ring-white\/\[0\.06\]{--tw-ring-color:hsla(0,0%,100%,.06)}.ring-zinc-500\/25{--tw-ring-color:hsla(240,4%,46%,.25)}.blur-3xl{--tw-blur:blur(64px)}.blur-3xl,.blur-\[100px\]{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)}.blur-\[100px\]{--tw-blur:blur(100px)}.blur-\[120px\]{--tw-blur:blur(120px)}.blur-\[120px\],.blur-xl{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)}.blur-xl{--tw-blur:blur(24px)}.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-2xl{--tw-backdrop-blur:blur(40px)}.backdrop-blur-2xl,.backdrop-blur-\[2px\]{-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-\[2px\]{--tw-backdrop-blur:blur(2px)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-lg,.backdrop-blur-md{-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)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur-sm,.backdrop-blur-xl{-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)}.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-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}.duration-300{transition-duration:.3s}.glass{border-width:1px;border-color:hsla(0,0%,100%,.08);--tw-shadow:0 25px 50px -12px rgba(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);--tw-shadow-color:rgba(0,0,0,.4);--tw-shadow:var(--tw-shadow-colored);background:var(--bg-card);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);transition:border-color .25s ease,box-shadow .25s ease,background .25s ease}html[data-theme=light] .glass{border-color:rgba(226,232,240,.9);--tw-shadow-color:rgba(148,163,184,.15);--tw-shadow:var(--tw-shadow-colored)}.section-title{font-size:1.125rem;line-height:1.75rem;font-weight:600;letter-spacing:-.025em;color:var(--text)}.section-lead{margin-top:.5rem}.meta-chip,.section-lead{font-size:.75rem;line-height:1rem;line-height:1.625;color:var(--muted)}.meta-chip{border-radius:1rem;border-width:1px;padding:.75rem 1rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;border-color:hsla(0,0%,100%,.1);background:rgba(0,0,0,.22)}html[data-theme=light] .meta-chip{border-color:rgba(226,232,240,.95);background:hsla(0,0%,100%,.72);color:var(--muted)}.tab-strip{margin-left:-.25rem;margin-right:-.25rem;display:flex;gap:.5rem;overflow-x:auto;padding-bottom:.375rem;padding-top:.125rem}@media (min-width:768px){.tab-strip{flex-wrap:wrap;overflow:visible}}.tab-strip{scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch}.tab-btn{flex-shrink:0;scroll-snap-align:start;border-radius:1rem;border-width:1px;padding:.625rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.tab-btn:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.tab-btn{--tw-ring-offset-color:var(--ring-offset);border-color:hsla(0,0%,100%,.12);background:rgba(0,0,0,.22);color:#cbd5e1}.tab-btn:hover{border-color:rgba(34,211,238,.25);color:#e2e8f0}.tab-btn-active{border-color:rgba(34,211,238,.45)!important;background:rgba(34,211,238,.12)!important;color:#cffafe!important}html[data-theme=light] .tab-btn{border-color:#e2e8f0;background:hsla(0,0%,100%,.75);color:#475569}html[data-theme=light] .tab-btn:hover{border-color:rgba(6,182,212,.45);color:#0f766e}html[data-theme=light] .tab-btn-active{border-color:rgba(6,182,212,.55)!important;background:rgba(207,250,254,.55)!important;color:#0e7490!important}.link-tile{display:block;border-radius:1rem;border-width:1px;padding:.625rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.link-tile:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.link-tile{--tw-ring-offset-color:var(--ring-offset);border-color:hsla(0,0%,100%,.1);background:rgba(0,0,0,.2);color:#f1f5f9}.link-tile:hover{border-color:rgba(34,211,238,.35)}html[data-theme=light] .link-tile{border-color:#e2e8f0;background:hsla(0,0%,100%,.65);color:#1e293b}.link-tile-accent{border-color:rgba(34,211,238,.28);background:rgba(34,211,238,.1);color:#ecfeff}.link-tile-accent:hover{border-color:rgba(34,211,238,.55)}html[data-theme=light] .link-tile-accent{border-color:rgba(6,182,212,.35);background:rgba(207,250,254,.65);color:#0e7490}.link-tile-warn{border-color:rgba(251,191,36,.25);background:rgba(251,191,36,.1);color:#fefce8}html[data-theme=light] .link-tile-warn{border-color:rgba(251,191,36,.45);background:hsla(55,92%,95%,.9);color:#78350f}.service-card{border-radius:1.5rem;border-width:1px;padding:1.25rem;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s;border-color:hsla(0,0%,100%,.08);background:rgba(0,0,0,.2)}.service-card:hover{box-shadow:0 12px 40px -24px rgba(34,211,238,.35)}html[data-theme=light] .service-card{border-color:rgba(226,232,240,.95);background:hsla(0,0%,100%,.55)}.panel-inset{border-radius:1rem;border-width:1px;padding:1rem;font-size:.875rem;line-height:1.25rem;border-color:hsla(0,0%,100%,.08);background:rgba(0,0,0,.2);color:#cbd5e1}html[data-theme=light] .panel-inset{border-color:rgba(226,232,240,.85);background:hsla(0,0%,100%,.5);color:#334155}.field-select{width:100%;border-radius:1rem;border-width:1px;padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem;outline:2px solid transparent;outline-offset:2px;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;border-color:hsla(0,0%,100%,.12);background:rgba(2,6,23,.55);color:#f8fafc}html[data-theme=light] .field-select{border-color:#e2e8f0;background:hsla(0,0%,100%,.9);color:#0f172a}.btn-focus:focus-visible{outline:2px solid transparent;outline-offset:2px;--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(2px + 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);--tw-ring-color:rgba(34,211,238,.5);--tw-ring-offset-width:2px}.btn-focus{--tw-ring-offset-color:var(--ring-offset)}.\[-ms-overflow-style\:none\]{-ms-overflow-style:none}.\[scrollbar-width\:none\]{scrollbar-width:none}:root{--bg-deep:#060912;--bg-card:rgba(16,22,36,.78);--accent:#22d3ee;--accent2:#a78bfa;--danger:#f43f5e;--ok:#34d399;--text:#e8edf7;--muted:#94a3b8;--ring-offset:#060912;color-scheme:dark}html[data-theme=light]{--bg-deep:#f1f5f9;--bg-card:hsla(0,0%,100%,.88);--text:#0f172a;--muted:#64748b;--ring-offset:#f1f5f9;color-scheme:light}html{scroll-behavior:smooth}body{color:var(--text);background:var(--bg-deep);min-height:100vh;font-family:var(--font-geist-sans),system-ui,sans-serif;overflow-x:hidden;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:background-color .35s ease,color .25s ease}*{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.35) transparent}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:rgba(148,163,184,.35);border-radius:999px}::-webkit-scrollbar-track{background:transparent}.app-bg-layer{position:fixed;inset:0;z-index:-1;opacity:.92;pointer-events:none;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(34,211,238,.22),transparent),radial-gradient(ellipse 60% 40% at 100% 0,rgba(167,139,250,.18),transparent),radial-gradient(ellipse 50% 30% at 0 100%,rgba(59,130,246,.12),transparent);transition:opacity .35s ease}html[data-theme=light] .app-bg-layer{opacity:.75;background:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(14,165,233,.12),transparent),radial-gradient(ellipse 60% 40% at 100% 0,rgba(139,92,246,.1),transparent),radial-gradient(ellipse 50% 30% at 0 100%,rgba(59,130,246,.08),transparent)}html[data-theme=light] .chart-surface{border-color:rgba(15,23,42,.12)!important;background:hsla(0,0%,100%,.65)!important}::selection{background:rgba(34,211,238,.25)}.safe-pb-nav{padding-bottom:calc(5.5rem + env(safe-area-inset-bottom, 0px))}@media (min-width:768px){.safe-pb-nav{padding-bottom:4rem}}@keyframes toast-in{0%{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%)}}.animate-toast-in{animation:toast-in .35s ease-out both}@keyframes pulse-soft{0%,to{opacity:1}50%{opacity:.65}}.animate-pulse-soft{animation:pulse-soft 2s ease-in-out infinite}.log-scroll{scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.35) transparent}.log-scroll::-webkit-scrollbar{width:6px;height:6px}.log-scroll::-webkit-scrollbar-thumb{background:rgba(148,163,184,.35);border-radius:6px}.portal-grid-bg{background-image:linear-gradient(hsla(0,0%,100%,.05) 1px,transparent 0),linear-gradient(90deg,hsla(0,0%,100%,.05) 1px,transparent 0);background-size:28px 28px}.lp-noise{background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E")}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:after,:before{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}.selection\:bg-violet-400\/25 ::selection{background-color:rgba(167,139,250,.25)}.selection\:bg-violet-500\/30 ::selection{background-color:rgba(139,92,246,.3)}.selection\:bg-violet-400\/25::selection{background-color:rgba(167,139,250,.25)}.selection\:bg-violet-500\/30::selection{background-color:rgba(139,92,246,.3)}.last\:border-0:last-child{border-width:0}.hover\:border-violet-400\/25:hover{border-color:rgba(167,139,250,.25)}.hover\:border-white\/15:hover{border-color:hsla(0,0%,100%,.15)}.hover\:border-white\/20:hover{border-color:hsla(0,0%,100%,.2)}.hover\:bg-black\/45:hover{background-color:rgba(0,0,0,.45)}.hover\:bg-cyan-500\/15:hover{background-color:rgba(6,182,212,.15)}.hover\:bg-cyan-500\/25:hover{background-color:rgba(6,182,212,.25)}.hover\:bg-emerald-500\/20:hover{background-color:rgba(16,185,129,.2)}.hover\:bg-slate-100:hover{--tw-bg-opacity:1;background-color:rgb(241 245 249/var(--tw-bg-opacity,1))}.hover\:bg-slate-200\/60:hover{background-color:rgba(226,232,240,.6)}.hover\:bg-slate-50:hover{--tw-bg-opacity:1;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.hover\:bg-violet-500\/25:hover{background-color:rgba(139,92,246,.25)}.hover\:bg-violet-500\/30:hover{background-color:rgba(139,92,246,.3)}.hover\:bg-white\/10:hover{background-color:hsla(0,0%,100%,.1)}.hover\:bg-white\/5:hover{background-color:hsla(0,0%,100%,.05)}.hover\:bg-white\/\[0\.04\]:hover{background-color:hsla(0,0%,100%,.04)}.hover\:bg-white\/\[0\.07\]:hover{background-color:hsla(0,0%,100%,.07)}.hover\:bg-white\/\[0\.08\]:hover{background-color:hsla(0,0%,100%,.08)}.hover\:bg-white\/\[0\.1\]:hover{background-color:hsla(0,0%,100%,.1)}.hover\:text-amber-300:hover{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.hover\:text-cyan-200:hover{--tw-text-opacity:1;color:rgb(165 243 252/var(--tw-text-opacity,1))}.hover\:text-rose-200:hover{--tw-text-opacity:1;color:rgb(254 205 211/var(--tw-text-opacity,1))}.hover\:text-slate-900:hover{--tw-text-opacity:1;color:rgb(15 23 42/var(--tw-text-opacity,1))}.hover\:text-zinc-200:hover{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.hover\:text-zinc-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-violet-400\/50:hover{--tw-ring-color:rgba(167,139,250,.5)}.hover\:brightness-110:hover{--tw-brightness:brightness(1.1);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)}.focus-visible\:ring-2:focus-visible{--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(2px + 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)}.focus-visible\:ring-emerald-500\/50:focus-visible{--tw-ring-color:rgba(16,185,129,.5)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}@media (min-width:640px){.sm\:block{display:block}.sm\:inline{display:inline}.sm\:w-auto{width:auto}.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\:flex-row{flex-direction:row}.sm\:flex-wrap{flex-wrap:wrap}.sm\:flex-nowrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-2{gap:.5rem}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (min-width:768px){.md\:bottom-6{bottom:1.5rem}.md\:hidden{display:none}.md\:h-72{height:18rem}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-start{align-items:flex-start}.lg\:items-end{align-items:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:px-10{padding-left:2.5rem;padding-right:2.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:py-8{padding-top:2rem;padding-bottom:2rem}.lg\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:1280px){.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-8{grid-column:span 8/span 8}.xl\:max-h-\[min\(24rem\2c 50vh\)\]{max-height:min(24rem,50vh)}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-\[0\.95fr\2c 1\.05fr\]{grid-template-columns:.95fr 1.05fr}.xl\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.xl\:grid-cols-\[18rem\2c 1fr\]{grid-template-columns:18rem 1fr}}.\[\&\:\:-webkit-scrollbar\]\:hidden::-webkit-scrollbar{display:none}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:border-cyan-600\/25{border-color:rgba(8,145,178,.25)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:border-slate-200\/60{border-color:rgba(226,232,240,.6)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:bg-white\/90{background-color:hsla(0,0%,100%,.9)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:bg-white\/95{background-color:hsla(0,0%,100%,.95)}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-cyan-700{--tw-text-opacity:1;color:rgb(14 116 144/var(--tw-text-opacity,1))}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-cyan-950{--tw-text-opacity:1;color:rgb(8 51 68/var(--tw-text-opacity,1))}html[data-theme=light] .\[html\[data-theme\=light\]_\&\]\:text-slate-600{--tw-text-opacity:1;color:rgb(71 85 105/var(--tw-text-opacity,1))} \ No newline at end of file diff --git a/d2ypp2/web-console/out/_next/static/KvIE-N5Gjg_4vh4xEAPuU/_buildManifest.js b/d2ypp2/web-console/out/_next/static/z_UuUy3mGDwiCWfhwZJVM/_buildManifest.js similarity index 100% rename from d2ypp2/web-console/out/_next/static/KvIE-N5Gjg_4vh4xEAPuU/_buildManifest.js rename to d2ypp2/web-console/out/_next/static/z_UuUy3mGDwiCWfhwZJVM/_buildManifest.js diff --git a/d2ypp2/web-console/out/_next/static/KvIE-N5Gjg_4vh4xEAPuU/_ssgManifest.js b/d2ypp2/web-console/out/_next/static/z_UuUy3mGDwiCWfhwZJVM/_ssgManifest.js similarity index 100% rename from d2ypp2/web-console/out/_next/static/KvIE-N5Gjg_4vh4xEAPuU/_ssgManifest.js rename to d2ypp2/web-console/out/_next/static/z_UuUy3mGDwiCWfhwZJVM/_ssgManifest.js diff --git a/d2ypp2/web-console/out/android.html b/d2ypp2/web-console/out/android.html index 035d1e0..4b38612 100644 --- a/d2ypp2/web-console/out/android.html +++ b/d2ypp2/web-console/out/android.html @@ -1 +1 @@ -无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file +无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file diff --git a/d2ypp2/web-console/out/android.txt b/d2ypp2/web-console/out/android.txt index 385c797..cbf9725 100644 --- a/d2ypp2/web-console/out/android.txt +++ b/d2ypp2/web-console/out/android.txt @@ -2,6 +2,6 @@ 3:I[3946,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","357","static/chunks/app/android/page-84550dabda18d658.js"],"default",1] 4:I[4707,[],""] 5:I[6423,[],""] -0:["KvIE-N5Gjg_4vh4xEAPuU",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/f9347d040ae16366.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] +0:["z_UuUy3mGDwiCWfhwZJVM",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] 6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]] 1:null diff --git a/d2ypp2/web-console/out/console.html b/d2ypp2/web-console/out/console.html index 253d969..0d8f67d 100644 --- a/d2ypp2/web-console/out/console.html +++ b/d2ypp2/web-console/out/console.html @@ -1 +1 @@ -无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file +无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file diff --git a/d2ypp2/web-console/out/console.txt b/d2ypp2/web-console/out/console.txt index cd148f0..4ba4be0 100644 --- a/d2ypp2/web-console/out/console.txt +++ b/d2ypp2/web-console/out/console.txt @@ -2,6 +2,6 @@ 3:I[6367,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","443","static/chunks/app/console/page-75d851a154677dbc.js"],"default",1] 4:I[4707,[],""] 5:I[6423,[],""] -0:["KvIE-N5Gjg_4vh4xEAPuU",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/f9347d040ae16366.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] +0:["z_UuUy3mGDwiCWfhwZJVM",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] 6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]] 1:null diff --git a/d2ypp2/web-console/out/index.html b/d2ypp2/web-console/out/index.html index 27b765a..712ccd6 100644 --- a/d2ypp2/web-console/out/index.html +++ b/d2ypp2/web-console/out/index.html @@ -1 +1 @@ -无人直播系统 · live.local

总览

live.local · 夜间 · 前台自动刷新 · 最后同步 未同步

直播工作室 · 3D

CPU 1m

负载均值

内存

/ —

磁盘可用

/ —

服务在线

基础服务层

运行时间

自开机

直播进程

  • 暂无直播进程
\ No newline at end of file +无人直播系统 · live.local

总览

live.local · 夜间 · 前台自动刷新 · 最后同步 未同步

直播工作室 · 3D

CPU 1m

负载均值

内存

/ —

磁盘可用

/ —

服务在线

基础服务层

运行时间

自开机

直播进程

  • 暂无直播进程
\ No newline at end of file diff --git a/d2ypp2/web-console/out/index.txt b/d2ypp2/web-console/out/index.txt index 6760a0b..99d54dc 100644 --- a/d2ypp2/web-console/out/index.txt +++ b/d2ypp2/web-console/out/index.txt @@ -1,7 +1,7 @@ 2:I[9107,[],"ClientPageRoot"] -3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-2e4163a91e299843.js"],"default",1] +3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-10361036b8bf1ca2.js"],"default",1] 4:I[4707,[],""] 5:I[6423,[],""] -0:["KvIE-N5Gjg_4vh4xEAPuU",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/f9347d040ae16366.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] +0:["z_UuUy3mGDwiCWfhwZJVM",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] 6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]] 1:null diff --git a/d2ypp2/web-console/out/shellcrash.html b/d2ypp2/web-console/out/shellcrash.html index d454bde..507010f 100644 --- a/d2ypp2/web-console/out/shellcrash.html +++ b/d2ypp2/web-console/out/shellcrash.html @@ -1 +1 @@ -无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file +无人直播系统 · live.local
返回门户首页无人直播 · 本页需与 API 同域(推荐 live.local:8001)

控制平面 · ARM Linux 开发板

无人直播控制中心

ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。

live.local · unknown · unknown
正在连接控制台...
\ No newline at end of file diff --git a/d2ypp2/web-console/out/shellcrash.txt b/d2ypp2/web-console/out/shellcrash.txt index b7b1d55..63c4234 100644 --- a/d2ypp2/web-console/out/shellcrash.txt +++ b/d2ypp2/web-console/out/shellcrash.txt @@ -2,6 +2,6 @@ 3:I[3398,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","6","static/chunks/app/shellcrash/page-3d8615ae919b9119.js"],"default",1] 4:I[4707,[],""] 5:I[6423,[],""] -0:["KvIE-N5Gjg_4vh4xEAPuU",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/f9347d040ae16366.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] +0:["z_UuUy3mGDwiCWfhwZJVM",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]] 6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台:YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]] 1:null diff --git a/d2ypp2/web.py b/d2ypp2/web.py index f943ce6..807147b 100644 --- a/d2ypp2/web.py +++ b/d2ypp2/web.py @@ -61,6 +61,7 @@ from src.config_governance import ( ) from src.hub_kv_store import get_youtube_pro_channels, set_youtube_pro_channels from src.hub_routes import YoutubeProChannelsBody, configure_hub, router as hub_router +from src.neko_capacity import ensure_neko_capacity_for_youtube_channels from src.process_runtime_status import summarize_process_activity, tail_log_text from src.process_runtime_status import coerce_declared_process_status, process_family from src.runtime_bootstrap import raise_nofile_limit @@ -552,7 +553,8 @@ async def alias_youtube_pro_channels_post(body: YoutubeProChannelsBody): set_youtube_pro_channels(body.channels) except ValueError as exc: return JSONResponse({"error": str(exc)}, status_code=400) - return {"message": "ok"} + capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, BASE_DIR, body.channels) + return {"message": "ok", "neko": capacity} app.add_middleware(