diff --git a/AGENTS.md b/AGENTS.md index 6e7339ee..2109841a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `bun scripts/cli.ts server rebuild `:以 build-first、Compose lock、no-deps force-recreate 和 post-up validation 的异步 job 重建主 server Compose 内单个服务;Code Queue 部署在 D601,规则见 `docs/reference/deployment.md`。 - `bun scripts/cli.ts provider attach [--master-server URL] [--up] [--force]`:在新增计算节点上生成两项配置的 provider-gateway 挂载包;默认只需要主 server URL(默认 `http://74.48.78.17/`)和唯一 Provider ID,生成的 Compose 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace`、SSH 维护私钥挂载和 loopback egress proxy 端口,规则见 `docs/reference/provider-gateway.md`。 - `bun scripts/cli.ts ssh [ssh-like args...]`:通过 provider-gateway 的 Host SSH / WSL SSH 维护桥打开近似原生 ssh 的交互会话或远端命令,并在远端 PATH 注入 `apply_patch`、`glob` 与 `skill-discover`;`apply-patch`、`py`、`skills`、结构化 `find`、`glob` 和 `argv` 子命令用于避免远端补丁、Python stdin、skill 发现与常用只读命令的嵌套转义问题,使用规则见 `docs/reference/cli.md` 和 `docs/reference/provider-gateway.md`。 -- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 v3s 控制面上的用户服务,OA Event Flow/Todo Note/Baidu Netdisk on main-server、V3S Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。 +- `bun scripts/cli.ts microservice list/status/health/proxy`:管理和验证挂载在主 server、计算节点 Docker 或 k3s 控制面上的用户服务,OA Event Flow/Todo Note/Baidu Netdisk on main-server、K3S Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 的规则见 `docs/reference/microservices.md`。 - `bun scripts/cli.ts deploy check/plan/apply [--file deploy.json] [--service ]`:按根目录 `deploy.json` 的服务 repo 和 commit 期望状态校验或更新用户服务,目标侧自行 fetch、构建、部署和 live commit 验证;规则见 `docs/reference/deploy.md`。 - `bun scripts/cli.ts codex deploy `:Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build 与 live commit 验证路径;规则见 `docs/reference/codex-deploy.md`。 - `bun scripts/cli.ts codex task `:按 Code Queue 任务 ID 查询初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,便于新任务引用历史 session。 @@ -40,12 +40,12 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 ## Runtime - `bun`:TypeScript 运行时固定使用 Bun,组件入口和 CLI 都直接运行 `.ts` 文件,约束见 `docs/reference/config.md`。 -- `docker-compose.yml`:主 server 统一编排 core、frontend、database、本机 provider gateway、Todo Note 后端、Baidu Netdisk 后端和 OA Event Flow 后端;Code Queue 和 MDTODO 由 D601 v3s/k8s 控制面代管,并经 `v3sctl-adapter` 的 Kubernetes API service proxy 单一路径接入,服务拓扑见 `docs/reference/deployment.md`。 -- `src/components/frontend`:前端源码固定使用 TypeScript + React,`app.tsx` 只做 shell/router,左侧主模块与顶部子标签统一编译为模块前缀路由:`/ops//`、`/nodes//`、`/tasks//`、`/config//`,只有用户服务使用 `/app//` 深链接,运行总览包含通用性能面板,资源监控含曲线和进程资源排序表,Todo Note、FindJob、Pipeline、MET Nonlinear、Baidu Netdisk、Code Queue、MDTODO、OA Event Flow、V3S Control 等业务页必须拆到独立 TSX 模块,界面规则见 `docs/reference/frontend.md`。 +- `docker-compose.yml`:主 server 统一编排 core、frontend、database、本机 provider gateway、Todo Note 后端、Baidu Netdisk 后端和 OA Event Flow 后端;Code Queue 和 MDTODO 由 D601 k3s/k8s 控制面代管,并经 `k3sctl-adapter` 的 Kubernetes API service proxy 单一路径接入,服务拓扑见 `docs/reference/deployment.md`。 +- `src/components/frontend`:前端源码固定使用 TypeScript + React,`app.tsx` 只做 shell/router,左侧主模块与顶部子标签统一编译为模块前缀路由:`/ops//`、`/nodes//`、`/tasks//`、`/config//`,只有用户服务使用 `/app//` 深链接,运行总览包含通用性能面板,资源监控含曲线和进程资源排序表,Todo Note、FindJob、Pipeline、MET Nonlinear、Baidu Netdisk、Code Queue、MDTODO、OA Event Flow、K3S Control 等业务页必须拆到独立 TSX 模块,界面规则见 `docs/reference/frontend.md`。 - `backend-core / frontend performance`:backend-core 暴露 `/api/performance`,frontend 暴露同源 `/api/frontend-performance` 并在 `/ops/performance/` 汇总组件请求、失败请求、内部操作和慢操作,规则见 `docs/reference/observability.md`。backend-core 源码已拆分为 15 个模块,结构见 `docs/reference/repo-tree.md`。 - `Unified OA event flow`:`oa-event-flow` 是独立主 server 用户服务,提供事件表、按 tag 订阅和 Trace/STEP 统计中心,Code Queue 与 Pipeline 都必须接入统一事件流;共享契约见 `docs/reference/oa-event-flow.md`,Pipeline 专有控制流规则见 `docs/reference/pipeline-oa-event-flow.md`。 - `src/components/provider-gateway`:当前主 server `74.48.78.17` 也作为 provider gateway 接入 UniDesk,外部节点通过 `ws://74.48.78.17:18082/ws/provider` 接入,必须以 `restart: always` 部署 always-enabled 远程升级、sleep-and-validate 回滚保护和 Host SSH / WSL SSH 透传并完成自测,部署与 Playwright 公网前端验证方法见 `docs/reference/provider-gateway.md`。 -- `microservices`:用户服务配置命名仍保留 `microservices`;用户服务指挂载在 UniDesk 核心服务上的用户业务能力,支持 `unidesk-direct` 与 `v3sctl-managed` 两种部署模式;v3s 代管必须使用标准 k3s/k8s 对象和 Kubernetes API service proxy,禁止业务容器直连、NodePort 和隐藏 fallback;缺少这些服务时核心仍可运行。主 server 本地开发边界固定为只开发 UniDesk frontend;非 UniDesk 核心业务后端、Dockerfile、GPU/训练调试必须在目标计算节点通过 SSH 透传或 v3s 控制面完成,Todo Note 这类明确写入主 server 的例外需单独登记,规则见 `docs/reference/microservices.md`。 +- `microservices`:用户服务配置命名仍保留 `microservices`;用户服务指挂载在 UniDesk 核心服务上的用户业务能力,支持 `unidesk-direct` 与 `k3sctl-managed` 两种部署模式;k3s 代管必须使用标准 k3s/k8s 对象和 Kubernetes API service proxy,禁止业务容器直连、NodePort 和隐藏 fallback;缺少这些服务时核心仍可运行。主 server 本地开发边界固定为只开发 UniDesk frontend;非 UniDesk 核心业务后端、Dockerfile、GPU/训练调试必须在目标计算节点通过 SSH 透传或 k3s 控制面完成,Todo Note 这类明确写入主 server 的例外需单独登记,规则见 `docs/reference/microservices.md`。 - `docs/reference/e2e.md`:交付前必须执行的自测门禁、Playwright 登录、资源监控进程排序、JSON 展示断言和数据库命名卷持久化要求。 ## Architecture Docs @@ -53,7 +53,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `docs/reference/arch.md`:UniDesk 分布式工作平台的长期架构约束。 - `docs/reference/repo-tree.md`:仓库结构目标与组件边界。 - `docs/reference/observability.md`:服务日志、任务活性、通用性能指标 API 和性能面板的可观测性规则。 -- `docs/reference/microservices.md`:用户服务(兼容命名 `microservice`)的配置、代理、安全边界、unidesk-direct/v3sctl-managed 部署模式、Todo Note/Baidu Netdisk on main-server、V3S Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 和验证规则。 +- `docs/reference/microservices.md`:用户服务(兼容命名 `microservice`)的配置、代理、安全边界、unidesk-direct/k3sctl-managed 部署模式、Todo Note/Baidu Netdisk on main-server、K3S Control/Code Queue/MDTODO/FindJob/Pipeline/MET Nonlinear on D601 和验证规则。 - `docs/reference/windows-passthrough.md`:WSL provider 通过 SSH 透传调用 Windows cmd/PowerShell、Keil、COM 串口和 Windows 侧 skill 的长期规则。 - `docs/reference/constar-d601.md`:D601 上 ConStart/constar 固件工作区的 UniDesk SSH 入口、WSL skill wrapper、Keil 编译下载和串口/JSON-RPC 验证简要引导。 - `docs/reference/oa-event-flow.md`:统一 OA 事件流微服务、事件表、tag 订阅、Trace/STEP 统计中心和前端可见性规则。 diff --git a/TEST.md b/TEST.md index 208b4b65..edb4ecfd 100644 --- a/TEST.md +++ b/TEST.md @@ -99,7 +99,7 @@ ## T23 D601 Code Queue User Service -阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、v3s/k8s `v3s://unidesk/code-queue:4222` 逻辑服务映射、`deployment.mode=v3sctl-managed`、`runtime.orchestrator=v3sctl` 且无业务直连容器摘要;使用 `bun scripts/cli.ts codex deploy <已push的commitId>` 重建/启动 D601 Code Queue,确认命令立即返回异步 job id,`bun scripts/cli.ts job status --tail-bytes 30000` 能看到 fetch/export、rsync、Docker build、k3s image import、kubectl apply、部署 commit 戳记、rollout 和 health commit 验证进度,并确认 job 最终校验真实 Code Queue `/health` 返回的 `deploy.commit` 精确匹配本次 remote commit,不能由旧服务或旧 Pod 充数;同时确认主 server 根目录 `docker-compose.yml` 中不再存在 `code-queue` service。运行 `bun scripts/cli.ts microservice health code-queue`、`bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw`、`bun scripts/cli.ts microservice proxy code-queue '/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId='` 和 `bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、v3sctl-adapter、Kubernetes API service proxy 和 D601 active Code Queue Service,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,`queue.storage.primary=postgres`、`queue.storage.postgresReady=true`、`queue.devReady.missingTools=[]`、`queue.devReady.docker.versionOk=true`、`queue.devReady.docker.composeOk=true`;`queue.devReady.ssh.ready` 只在需要跨 Provider SSH/Windows-native 任务时作为强制项。在 D601 `code-queue-backend` 容器内验证主 PostgreSQL 端口映射可执行 `select 1`,主 OA Event Flow 端口映射 `/health` 可访问,本机 ClaudeQQ `http://host.docker.internal:3290/health` 可访问;这些映射不得成为任意公网入口。 +阅读 `AGENTS.md`(本项目 `AGENTS.md` 同时承担 `SKILL.md` 对 `scripts/cli.ts` 的解释职责),然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts microservice list`,确认 `code-queue` 显示为 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL `https://github.com/pikasTech/unidesk`、k3s/k8s `k3s://unidesk/code-queue:4222` 逻辑服务映射、`deployment.mode=k3sctl-managed`、`runtime.orchestrator=k3sctl` 且无业务直连容器摘要;使用 `bun scripts/cli.ts codex deploy <已push的commitId>` 重建/启动 D601 Code Queue,确认命令立即返回异步 job id,`bun scripts/cli.ts job status --tail-bytes 30000` 能看到 fetch/export、rsync、Docker build、k3s image import、kubectl apply、部署 commit 戳记、rollout 和 health commit 验证进度,并确认 job 最终校验真实 Code Queue `/health` 返回的 `deploy.commit` 精确匹配本次 remote commit,不能由旧服务或旧 Pod 充数;同时确认主 server 根目录 `docker-compose.yml` 中不再存在 `code-queue` service。运行 `bun scripts/cli.ts microservice health code-queue`、`bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw`、`bun scripts/cli.ts microservice proxy code-queue '/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId='` 和 `bun scripts/cli.ts codex task <已有taskId>`,确认链路通过 backend-core、k3sctl-adapter、Kubernetes API service proxy 和 D601 active Code Queue Service,且 task id 查询返回初始 prompt、最后 assistant message、工具调用摘要、attempt/judge/error 和耗时,`queue.storage.primary=postgres`、`queue.storage.postgresReady=true`、`queue.devReady.missingTools=[]`、`queue.devReady.docker.versionOk=true`、`queue.devReady.docker.composeOk=true`;`queue.devReady.ssh.ready` 只在需要跨 Provider SSH/Windows-native 任务时作为强制项。在 D601 `code-queue-backend` 容器内验证主 PostgreSQL 端口映射可执行 `select 1`,主 OA Event Flow 端口映射 `/health` 可访问,本机 ClaudeQQ `http://host.docker.internal:3290/health` 可访问;这些映射不得成为任意公网入口。 随后登录公网 frontend `http://74.48.78.17:18081/`,进入 `用户服务 / Code Queue`,确认页面显示默认模型 `gpt-5.5`、默认执行 Provider `D601`、默认工作目录 `/workspace`、模型下拉菜单包含 `gpt-5.4-mini`/`gpt-5.4`/`gpt-5.5`、入队份数、队列指标、任务 ID、复制任务 ID、引用按钮、任务耗时、引用任务 ID、清空输入、创建成功提示、任务提交表单、Trace 输出、attempt 表、MiniMax/fallback judge 状态、追加 prompt、打断和重试控件;通过页面提交一个小任务,确认任务进入 queued/running/succeeded 或可解释的 failed 状态,并且输出区能看到运行中的 Codex 消息。批量验收时设置 `入队份数=5` 或用 `---` 分隔 5 段 prompt,一次性入队 5 条任务,确认 5 条任务按顺序运行并全部进入 succeeded 或可解释的非成功终态,不能只运行第一条后停止;其中任一任务被 judge 判定 `fail` 时只能把当前任务标为 failed,后续 queued 任务仍必须继续推进。测试异常中断时可以提交长任务后点击 `打断`,确认任务变为 canceled 或被 judge 标记为非成功终态;自动重试只应在服务端/传输异常、任务正常结束但 execution record 显示未完成、或 judge 判定 retry 时发生;retry 必须复用已有 Codex thread 并 append 继续执行 prompt,只有当前任务 complete 后才推进队列中的下一个任务。MiniMax judge 必须能处理 Markdown fence/夹杂文本等 JSON 去噪;若去噪后仍失败,必须把解析错误和上一轮去噪前原始回答反馈给 MiniMax 修复后重试,日志中应出现 `judge_json_parse_retry`,且 repair 成功时仍以 `source=minimax` 返回。Codex provider key 只能通过 `OPENAI_API_KEY`、`CRS_OAI_KEY` 这类运行时环境透传,MiniMax API key 只能通过 D601 env-file 运行时环境传入,禁止写入 `config.json`、Dockerfile、源码或测试文档。 diff --git a/config.json b/config.json index d2a76fdc..ad93c750 100644 --- a/config.json +++ b/config.json @@ -534,17 +534,17 @@ } }, { - "id": "v3sctl-adapter", - "name": "V3S Control Plane", + "id": "k3sctl-adapter", + "name": "K3S Control Plane", "providerId": "D601", - "description": "v3sctl-adapter 是 UniDesk 直管的 v3s 控制平面适配微服务,连接 D601 原生 v3s/v3sctl 控制平面,并通过 v3s 标准服务路由代理 D601/D518 等节点上的代管微服务。", + "description": "k3sctl-adapter 是 UniDesk 直管的 k3s 控制平面适配微服务,连接 D601 原生 k3s/k3sctl 控制平面,并通过 k3s 标准服务路由代理 D601/D518 等节点上的代管微服务。", "repository": { "url": "https://github.com/pikasTech/unidesk", "commitId": "local", - "dockerfile": "src/components/microservices/v3sctl-adapter/Dockerfile", - "composeFile": "src/components/microservices/v3sctl-adapter/docker-compose.d601.yml", - "composeService": "v3sctl-adapter", - "containerName": "v3sctl-adapter" + "dockerfile": "src/components/microservices/k3sctl-adapter/Dockerfile", + "composeFile": "src/components/microservices/k3sctl-adapter/docker-compose.d601.yml", + "composeService": "k3sctl-adapter", + "containerName": "k3sctl-adapter" }, "backend": { "nodeBaseUrl": "http://host.docker.internal:4266", @@ -577,7 +577,7 @@ "worktreePath": "/home/ubuntu/cq-deploy" }, "frontend": { - "route": "/apps/v3sctl", + "route": "/apps/k3sctl", "integrated": true } }, @@ -585,20 +585,20 @@ "id": "code-queue", "name": "Code Queue", "providerId": "D601", - "description": "Code Queue 是由 D601 v3s 控制平面代管的代码代理队列用户服务,UniDesk 只通过 v3sctl-adapter 访问其标准服务路由;D601/D518 实例由 v3s 管理,provider-gateway 只保留维护用途。", + "description": "Code Queue 是由 D601 k3s 控制平面代管的代码代理队列用户服务,UniDesk 只通过 k3sctl-adapter 访问其标准服务路由;D601/D518 实例由 k3s 管理,provider-gateway 只保留维护用途。", "repository": { "url": "https://github.com/pikasTech/unidesk", "commitId": "cbbed004a6c84a7dbc554bb90692bd80b4384e67", "dockerfile": "src/components/microservices/code-queue/Dockerfile", - "composeFile": "src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json", + "composeFile": "src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json", "composeService": "code-queue", - "containerName": "v3s:code-queue" + "containerName": "k3s:code-queue" }, "backend": { - "nodeBaseUrl": "v3s://code-queue", - "nodeBindHost": "v3s://unidesk/code-queue", + "nodeBaseUrl": "k3s://code-queue", + "nodeBindHost": "k3s://unidesk/code-queue", "nodePort": 4222, - "proxyMode": "v3sctl-adapter-http", + "proxyMode": "k3sctl-adapter-http", "frontendOnly": true, "public": false, "allowedMethods": [ @@ -626,9 +626,9 @@ "integrated": true }, "deployment": { - "mode": "v3sctl-managed", - "adapterServiceId": "v3sctl-adapter", - "v3sServiceId": "code-queue", + "mode": "k3sctl-managed", + "adapterServiceId": "k3sctl-adapter", + "k3sServiceId": "code-queue", "namespace": "unidesk", "expectedNodeIds": [ "D601", @@ -641,20 +641,20 @@ "id": "mdtodo", "name": "MDTODO", "providerId": "D601", - "description": "MDTODO 是由 D601 k3s/v3s 控制面代管的 Markdown TODO 后端,读取原 F:\\Work\\vscode-mdtodo 工作区,UniDesk frontend 负责统一任务树、编辑和状态展示。", + "description": "MDTODO 是由 D601 k3s 控制面代管的 Markdown TODO 后端,读取原 F:\\Work\\vscode-mdtodo 工作区,UniDesk frontend 负责统一任务树、编辑和状态展示。", "repository": { "url": "https://github.com/pikasTech/unidesk", "commitId": "local", "dockerfile": "src/components/microservices/mdtodo/Dockerfile", - "composeFile": "src/components/microservices/v3sctl-adapter/v3s/mdtodo.v3s.json", + "composeFile": "src/components/microservices/k3sctl-adapter/k3s/mdtodo.k3s.json", "composeService": "mdtodo", - "containerName": "v3s:mdtodo" + "containerName": "k3s:mdtodo" }, "backend": { - "nodeBaseUrl": "v3s://mdtodo", - "nodeBindHost": "v3s://unidesk/mdtodo", + "nodeBaseUrl": "k3s://mdtodo", + "nodeBindHost": "k3s://unidesk/mdtodo", "nodePort": 4267, - "proxyMode": "v3sctl-adapter-http", + "proxyMode": "k3sctl-adapter-http", "frontendOnly": true, "public": false, "allowedMethods": [ @@ -684,9 +684,9 @@ "integrated": true }, "deployment": { - "mode": "v3sctl-managed", - "adapterServiceId": "v3sctl-adapter", - "v3sServiceId": "mdtodo", + "mode": "k3sctl-managed", + "adapterServiceId": "k3sctl-adapter", + "k3sServiceId": "mdtodo", "namespace": "unidesk", "expectedNodeIds": [ "D601" diff --git a/deploy.json b/deploy.json index 95896486..047a2592 100644 --- a/deploy.json +++ b/deploy.json @@ -42,7 +42,7 @@ "commitId": "0c3cdb4ee06a23361ed511a2da033d67b53d16f4" }, { - "id": "v3sctl-adapter", + "id": "k3sctl-adapter", "repo": "https://github.com/pikasTech/unidesk", "commitId": "0c3cdb4ee06a23361ed511a2da033d67b53d16f4" }, diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ed1d34ba..7aa16b03 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -19,7 +19,7 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 - `ssh py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。 - `ssh skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。 - `microservice list/status/health/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 中的用户服务(底层命令名仍为 microservice);`health` 和 `proxy` 会走真实 backend-core -> provider-gateway -> 节点本机后端链路,`proxy` 对超大 body 默认输出有界预览,规则见 `docs/reference/microservices.md`。 -- `deploy check/plan/apply` 从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新直管服务与 v3s/k3s 代管服务;规则见 `docs/reference/deploy.md`。 +- `deploy check/plan/apply` 从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新直管服务与 k3s 代管服务;规则见 `docs/reference/deploy.md`。 - `codex deploy ` 是 Code Queue 兼容部署入口,会生成临时 desired manifest 并调用 `deploy apply --service code-queue` 的同一条 target-side build、k3s import、rollout 和 live commit 验证路径;详细规则见 `docs/reference/codex-deploy.md`。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。 - `codex task --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`。 @@ -34,7 +34,7 @@ UniDesk 的统一 CLI 入口是根目录 `scripts/cli.ts`,运行方式固定 长时操作采用 Fire-and-Forget 模式:CLI 创建 `.state/jobs/{jobId}.json`,后台进程执行真实命令,并将 stdout、stderr 分别写入 `.state/jobs/{jobId}.stdout.log` 与 `.state/jobs/{jobId}.stderr.log`。调用者通过 `bun scripts/cli.ts job status ` 查询进度和尾部输出。 -`server rebuild` 与 `server start`、`server stop` 一样必须通过返回的 job id 确认结果;不要把连续 `server rebuild` 命令理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 的标准流程是运行 `bun scripts/cli.ts server rebuild frontend`,随后轮询 `bun scripts/cli.ts job status ` 到 `succeeded`,再用 `server status` 或 `e2e run` 验证公网 frontend;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note` 和 `microservice proxy todo-note /api/instances` 验证;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager` 和 `microservice proxy project-manager /api/projects` 验证;重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`,随后用 `microservice health baidu-netdisk` 和 `microservice proxy baidu-netdisk /api/transfers` 验证;重建 OA Event Flow 后端使用 `bun scripts/cli.ts server rebuild oa-event-flow`,随后用 `microservice health oa-event-flow` 和 `microservice proxy oa-event-flow /api/diagnostics` 验证。Code Queue 后端由 D601 v3s/k8s 控制面代管,必须使用 `bun scripts/cli.ts deploy apply --service code-queue` 或兼容入口 `bun scripts/cli.ts codex deploy ` 部署已 push 的 remote commit;部署 job 自身必须通过真实 `/health` 和 k3s Deployment annotation 证明不是旧服务在充数,之后再用 `microservice health code-queue` 和 `microservice proxy code-queue /api/tasks/overview` 做人工复核。不得把 `docker rm` 手工兜底当成正式交付步骤。 +`server rebuild` 与 `server start`、`server stop` 一样必须通过返回的 job id 确认结果;不要把连续 `server rebuild` 命令理解成“前一个重建已完成”,因为两个命令只是在快速创建异步 job。重建 frontend 的标准流程是运行 `bun scripts/cli.ts server rebuild frontend`,随后轮询 `bun scripts/cli.ts job status ` 到 `succeeded`,再用 `server status` 或 `e2e run` 验证公网 frontend;重建 Todo Note 后端使用 `bun scripts/cli.ts server rebuild todo-note`,随后用 `microservice health todo-note` 和 `microservice proxy todo-note /api/instances` 验证;重建 Project Manager 后端使用 `bun scripts/cli.ts server rebuild project-manager`,随后用 `microservice health project-manager` 和 `microservice proxy project-manager /api/projects` 验证;重建 Baidu Netdisk 后端使用 `bun scripts/cli.ts server rebuild baidu-netdisk`,随后用 `microservice health baidu-netdisk` 和 `microservice proxy baidu-netdisk /api/transfers` 验证;重建 OA Event Flow 后端使用 `bun scripts/cli.ts server rebuild oa-event-flow`,随后用 `microservice health oa-event-flow` 和 `microservice proxy oa-event-flow /api/diagnostics` 验证。Code Queue 后端由 D601 k3s/k8s 控制面代管,必须使用 `bun scripts/cli.ts deploy apply --service code-queue` 或兼容入口 `bun scripts/cli.ts codex deploy ` 部署已 push 的 remote commit;部署 job 自身必须通过真实 `/health` 和 k3s Deployment annotation 证明不是旧服务在充数,之后再用 `microservice health code-queue` 和 `microservice proxy code-queue /api/tasks/overview` 做人工复核。不得把 `docker rm` 手工兜底当成正式交付步骤。 新部署入口优先使用 `deploy apply`。旧的 `server rebuild` 和 `codex deploy` 只保留为兼容入口,后续实现应收敛到同一个 reconciler:从 remote commit 导出源码,在目标节点一次性代理构建镜像,部署后用 live commit 校验证明不是旧服务。 diff --git a/docs/reference/codex-deploy.md b/docs/reference/codex-deploy.md index 42a15c1c..806cdb4e 100644 --- a/docs/reference/codex-deploy.md +++ b/docs/reference/codex-deploy.md @@ -23,8 +23,8 @@ bun scripts/cli.ts job status --tail-bytes 30000 2. 在 D601 的 deploy cache 中通过本机 provider-gateway WS egress proxy 执行 `git fetch` remote,并用 `git archive ` 导出 tracked files 到一次性 export 目录;不得让 D601 直连 GitHub,也不得临时创建 SSH SOCKS、公网 master proxy 或 backend-core/provider-ingress fallback。 3. 用 `rsync --delete` 同步导出的 repo 到 `/home/ubuntu/cq-deploy`,保留 `.state/`、`logs/`、`.git/`、`node_modules/` 和 `dist/`。 4. 在 D601 用目标 Docker daemon 的本地 BuildKit builder 构建 `unidesk-code-queue:d601`,复用 D601 上已有基础镜像、inline cache 和 Code Queue build-base;provider-gateway WS egress 是唯一允许的构建代理通道,只作为本次 build 的环境变量与 build-arg 注入,并配合本次 build 的 `--network host` 让 RUN 阶段访问 D601 宿主 loopback proxy,不能污染 D601 宿主 Docker/HTTP proxy 配置,不能新建 SSH SOCKS、公网 master proxy 或直连 fallback。 -5. `docker save` 镜像并导入 k3s containerd:`docker exec -i unidesk-v8s-server ctr -n k8s.io images import -`。 -6. `kubectl apply -f src/components/microservices/v3sctl-adapter/v3s/code-queue.k8s.yaml`,其中包含 Code Queue 和 `d601-tcp-egress-gateway`。 +5. `docker save` 镜像并导入 k3s containerd:`docker exec -i unidesk-k3s-server ctr -n k8s.io images import -`。 +6. `kubectl apply -f src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml`,其中包含 Code Queue 和 `d601-tcp-egress-gateway`。 7. 将解析后的 40 位 remote commit 写入 `deployment/code-queue` 的 `CODE_QUEUE_DEPLOY_COMMIT` / `CODE_QUEUE_DEPLOY_REQUESTED_COMMIT`,并记录到 Deployment annotation。 8. `kubectl -n unidesk rollout restart deployment/d601-tcp-egress-gateway deployment/code-queue` 并等待 rollout 完成。 9. 通过 backend-core 的真实微服务代理读取 Code Queue `/health`,强制校验 `deploy.commit` 等于本次解析出的 remote commit;如果健康的是旧服务或旧 Pod,job 必须失败。 @@ -42,7 +42,7 @@ bun scripts/cli.ts microservice proxy code-queue '/api/tasks/overview?limit=5&tr ## Boundaries -Code Queue 由 D601 v3s/k8s 控制面代管,不再通过 `server rebuild` 或手工 `docker compose up` 作为正式部署路径。`codex deploy` 可以在 Code Queue 自身正在执行任务时运行;服务重启后由 restart-recovery 恢复任务状态,不能等待当前 Code Queue task 退出后再部署。 +Code Queue 由 D601 k3s/k8s 控制面代管,不再通过 `server rebuild` 或手工 `docker compose up` 作为正式部署路径。`codex deploy` 可以在 Code Queue 自身正在执行任务时运行;服务重启后由 restart-recovery 恢复任务状态,不能等待当前 Code Queue task 退出后再部署。 ## TCP Egress Gateway diff --git a/docs/reference/config.md b/docs/reference/config.md index a103d069..c73e0080 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -26,7 +26,7 @@ TypeScript 运行时固定为 Bun。根目录 CLI、backend-core、frontend 和 `microservices` 定义挂载在计算节点或主 server Docker 中的非核心用户服务。用户服务是挂在 UniDesk 核心服务上的用户业务能力,缺少这些服务时 UniDesk 核心仍必须能运行。该数组只保存业务仓库 URL、commit id、业务仓库自身 Dockerfile/docker-compose 引用、provider 映射、节点后端端口和 UniDesk frontend 集成入口;不得把业务全量代码复制进 UniDesk。`backend.public` 必须为 `false`,`backend.frontendOnly` 必须为 `true`,`backend.allowedPathPrefixes` 必须限制到业务 API 前缀,`backend.allowedMethods` 必须显式列出允许代理的 HTTP 方法;浏览器只能通过 frontend 同源代理访问这些后端。详细规则见 `docs/reference/microservices.md`。 -主 server 承载的 Todo Note 用户服务使用 `providerId=main-server`、`nodeBaseUrl=http://todo-note:4211` 和 `allowedMethods=["GET","HEAD","POST","DELETE"]`,数据库使用主 PostgreSQL;Code Queue 使用 `deployment.mode=v3sctl-managed`、`backend.proxyMode=v3sctl-adapter-http` 和 `nodeBaseUrl=v3s://code-queue`,只允许通过 frontend/backend-core -> `v3sctl-adapter` -> Kubernetes API service proxy -> v3s/k8s Service 单一路径访问,不能配置业务容器直连、NodePort 或 provider-gateway direct path;D601 的 FindJob 只允许 `GET/HEAD` 展示型读取路径;D601 的 Pipeline 允许 `GET/HEAD/POST`,其中 `POST` 只用于 Pipeline 后端 `/api/node-control/...` 的 append prompt、guide 和 redo/restart 等受控 node 操作。 +主 server 承载的 Todo Note 用户服务使用 `providerId=main-server`、`nodeBaseUrl=http://todo-note:4211` 和 `allowedMethods=["GET","HEAD","POST","DELETE"]`,数据库使用主 PostgreSQL;Code Queue 使用 `deployment.mode=k3sctl-managed`、`backend.proxyMode=k3sctl-adapter-http` 和 `nodeBaseUrl=k3s://code-queue`,只允许通过 frontend/backend-core -> `k3sctl-adapter` -> Kubernetes API service proxy -> k3s/k8s Service 单一路径访问,不能配置业务容器直连、NodePort 或 provider-gateway direct path;D601 的 FindJob 只允许 `GET/HEAD` 展示型读取路径;D601 的 Pipeline 允许 `GET/HEAD/POST`,其中 `POST` 只用于 Pipeline 后端 `/api/node-control/...` 的 append prompt、guide 和 redo/restart 等受控 node 操作。 ## Compose Env Generation diff --git a/docs/reference/deploy.md b/docs/reference/deploy.md index 264e2378..7af485b2 100644 --- a/docs/reference/deploy.md +++ b/docs/reference/deploy.md @@ -19,7 +19,7 @@ The root `deploy.json` is intentionally minimal: } ``` -`deploy.json` must not contain provider IDs, ports, compose service names, Kubernetes namespace, health paths, environment variables, Dockerfile paths or build commands. The deploy reconciler joins each `id` with `config.json.microservices[]` and existing v3s manifests to resolve those details. A service listed in `deploy.json` but missing from `config.json` is an error. A service with no Dockerfile source artifact is reported as unsupported rather than silently skipped. +`deploy.json` must not contain provider IDs, ports, compose service names, Kubernetes namespace, health paths, environment variables, Dockerfile paths or build commands. The deploy reconciler joins each `id` with `config.json.microservices[]` and existing k3s manifests to resolve those details. A service listed in `deploy.json` but missing from `config.json` is an error. A service with no Dockerfile source artifact is reported as unsupported rather than silently skipped. `config.json.microservices[].repository.commitId` is retained for catalog compatibility, but `deploy.json` is the deployment version authority for the reconciler. @@ -40,7 +40,7 @@ Target-side build is the only standard deployment mode. The controller may run o - Main server services are fetched, built and deployed on the main server. - D601 services are fetched, built and deployed on D601. - D518 services are fetched, built and deployed on D518. -- v3s/k3s managed services are built on the active control target and then imported into that target's Kubernetes container runtime. +- k3s managed services are built on the active control target and then imported into that target's Kubernetes container runtime. The reconciler distributes only repository URL, commit ID, Dockerfile path, build context and the existing deployment manifest/compose declaration. It must not distribute large Docker images between hosts as the default path, and it must not accept `docker commit` images, dirty worktrees or hand-mutated runtime containers as deployment truth. @@ -68,7 +68,7 @@ The reconciler selects the executor from `config.json`: - `deployment.mode=unidesk-direct` on `main-server`: build the image on the main server, then use the fixed UniDesk Compose project and `up -d --no-build --no-deps --force-recreate `. - `deployment.mode=unidesk-direct` on a provider: dispatch `host.ssh` to that provider, build on the provider, then use the service's provider-local compose file and project. The executor resolves the actual Compose project, image name, build context, Dockerfile and target from the running container labels and `docker compose config`; it must not guess an image tag that the service will not actually run. -- `deployment.mode=v3sctl-managed`: dispatch to the active control target, build on that target, import the image into k3s/containerd, apply the existing Kubernetes manifest, stamp the Deployment and wait for rollout. +- `deployment.mode=k3sctl-managed`: dispatch to the active control target, build on that target, import the image into k3s/containerd, apply the existing Kubernetes manifest, stamp the Deployment and wait for rollout. Existing service-specific commands such as Code Queue deploy should converge onto this reconciler path instead of keeping a parallel implementation. @@ -80,7 +80,7 @@ Every successful deployment must stamp the source version in the runtime: - Runtime env or Kubernetes annotations: `UNIDESK_DEPLOY_SERVICE_ID`, `UNIDESK_DEPLOY_REPO`, `UNIDESK_DEPLOY_COMMIT` and `UNIDESK_DEPLOY_REQUESTED_COMMIT`. - Service health response should expose `deploy.repo` and `deploy.commit` when practical. Existing service-specific health contracts such as Code Queue's `deploy.commit` remain valid. -The deploy job is not complete until live verification proves the running service matches the requested commit. For Docker services this includes image label inspection on the running container. For v3s/k3s services this includes Deployment annotation/env inspection and service health through the same UniDesk microservice proxy path used by the frontend. A healthy old service must fail verification. +The deploy job is not complete until live verification proves the running service matches the requested commit. For Docker services this includes image label inspection on the running container. For k3s services this includes Deployment annotation/env inspection and service health through the same UniDesk microservice proxy path used by the frontend. A healthy old service must fail verification. ## Unsupported Services diff --git a/docs/reference/deployment.md b/docs/reference/deployment.md index 3954799c..91ec99e8 100644 --- a/docs/reference/deployment.md +++ b/docs/reference/deployment.md @@ -1,6 +1,6 @@ # UniDesk Deployment Reference -主 server 使用根目录 `docker-compose.yml` 统一编排 database、backend-core、frontend、provider-gateway 以及必须留在主 server 的用户服务。当前环境本身就是主 server,因此 provider-gateway 也在同一台机器上启动,用与普通计算节点相同的 WebSocket 方式接入 core。Code Queue 不再属于主 server Compose,也不再由 backend-core 通过 provider-gateway 直连业务容器;它作为 `v3sctl-managed` 用户服务经 D601 `v3sctl-adapter` 进入 v3s 标准服务路由。 +主 server 使用根目录 `docker-compose.yml` 统一编排 database、backend-core、frontend、provider-gateway 以及必须留在主 server 的用户服务。当前环境本身就是主 server,因此 provider-gateway 也在同一台机器上启动,用与普通计算节点相同的 WebSocket 方式接入 core。Code Queue 不再属于主 server Compose,也不再由 backend-core 通过 provider-gateway 直连业务容器;它作为 `k3sctl-managed` 用户服务经 D601 `k3sctl-adapter` 进入 k3s 标准服务路由。 ## Services @@ -9,8 +9,8 @@ - `frontend` 是唯一公开 Web 控制台,提供登录、从 TSX 转译出的 React 应用资产和到 backend-core 的同源代理。 - `provider-gateway` 是当前主 server 的本机计算节点代理,通过 WebSocket 主动连到 provider ingress,挂载 `/var/run/docker.sock` 作为自动任务执行主路径,使用 `pid: "host"` 读取节点级进程资源,并周期性上报系统资源指标、进程占用与 Docker daemon 状态;计算节点 provider-gateway 还必须把 egress proxy 仅发布到宿主 loopback `127.0.0.1:18789` 供本节点执行环境出网,维护用 Host SSH / WSL SSH 私钥目录只读挂载到 `/run/host-ssh`,不得作为自动任务调度主路径。 - `todo-note` 是主 server 承载的 Todo Note 纯后端用户服务,容器名 `todo-note-backend`,只在 Compose 内网暴露 `4211/tcp`,使用主 PostgreSQL 存储迁移后的 Todo Note 数据。 -- `v3sctl-adapter` 是 D601 上由 UniDesk 直管的 v3s 控制面适配微服务,容器名 `v3sctl-adapter`,只绑定 `127.0.0.1:4266`,由 UniDesk frontend/backend-core 通过用户服务代理访问并提供 `/api/control-plane` 可见性。 -- `code-queue` 是由 D601 v3s/k8s 控制面代管的 Codex/OpenCode 队列用户服务,active/standby 实例通过 `src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json` 声明,运行对象通过 `code-queue.k8s.yaml` 创建 Kubernetes Deployment/ClusterIP Service;任务、queue、未读状态、控制状态和通知 outbox 一律写入主 PostgreSQL,不保留本地状态文件 fallback,浏览器只能通过 UniDesk frontend -> backend-core -> v3sctl-adapter -> Kubernetes API service proxy -> Code Queue Service 查看运行输出、追加 prompt、打断和重试。 +- `k3sctl-adapter` 是 D601 上由 UniDesk 直管的 k3s 控制面适配微服务,容器名 `k3sctl-adapter`,只绑定 `127.0.0.1:4266`,由 UniDesk frontend/backend-core 通过用户服务代理访问并提供 `/api/control-plane` 可见性。 +- `code-queue` 是由 D601 k3s/k8s 控制面代管的 Codex/OpenCode 队列用户服务,active/standby 实例通过 `src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json` 声明,运行对象通过 `code-queue.k8s.yaml` 创建 Kubernetes Deployment/ClusterIP Service;任务、queue、未读状态、控制状态和通知 outbox 一律写入主 PostgreSQL,不保留本地状态文件 fallback,浏览器只能通过 UniDesk frontend -> backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> Code Queue Service 查看运行输出、追加 prompt、打断和重试。 - `project-manager` 是主 server 承载的项目管理用户服务,容器名 `project-manager-backend`,仅在 Compose 内网暴露 `4233/tcp`,项目清单写入主 PostgreSQL,浏览器只能通过 UniDesk frontend 同源代理执行增删改查、Excel 导入和 Excel 导出。 - `baidu-netdisk` 是主 server 承载的百度网盘存储用户服务,容器名 `baidu-netdisk-backend`,仅在 Compose 内网暴露 `4244/tcp`,OAuth/token/transfer 状态写入主 PostgreSQL,浏览器只能通过 UniDesk frontend 同源代理执行设备码登录、文件浏览和 staging 传输任务控制。 @@ -52,11 +52,11 @@ frontend 的 Docker 上线顺序为:先运行必要的本地校验,例如 `b ## Health Criteria -服务跑通的最低标准是:backend-core 内网 `/health` 返回 ok,frontend 公网 `/health` 返回 ok,provider ingress 公网 `/health` 返回 ok,database 在容器内 `pg_isready` 可用,Todo Note 后端 `/api/health` 返回 `storage=postgres`,`v3sctl-adapter` `/api/control-plane` 可见 `unidesk-v8s` Kubernetes API service proxy 状态、D601 active serving healthy、D518 standby pod ready、`presentNodeIds=[D601,D518]`、`missingNodeIds=[]` 和 no-fallback 路径,Code Queue `/health` 经 v3s active Service 返回轻量 readiness、默认模型、`queue.storage` 和 `egressProxy.connected=true`,`/api/tasks/overview` 返回 PostgreSQL 队列总览,Project Manager `/health` 返回 `storage.primary=postgres` 和项目数量,backend-core `/api/performance` 返回性能指标,`/api/nodes` 中出现 `main-server`、`D601` 和 `D518` provider 且状态为 `online`,`/api/nodes/system-status` 中出现对应 CPU/内存/硬盘采样,`/api/nodes/docker-status` 中能看到主 server、D601 与 D518 Docker 快照。D518 必须通过 K3S agent 加入 V8S 控制面并运行 `code-queue-d518` standby Pod;不得用 D601->D518 直连、NodePort 或 provider-gateway business HTTP 绕过 Kubernetes service route。交付前还必须运行 `bun scripts/cli.ts e2e run`,并以 `docs/reference/e2e.md` 的门禁作为最终判定。 +服务跑通的最低标准是:backend-core 内网 `/health` 返回 ok,frontend 公网 `/health` 返回 ok,provider ingress 公网 `/health` 返回 ok,database 在容器内 `pg_isready` 可用,Todo Note 后端 `/api/health` 返回 `storage=postgres`,`k3sctl-adapter` `/api/control-plane` 可见 `unidesk-k3s` Kubernetes API service proxy 状态、D601 active serving healthy、D518 standby pod ready、`presentNodeIds=[D601,D518]`、`missingNodeIds=[]` 和 no-fallback 路径,Code Queue `/health` 经 k3s active Service 返回轻量 readiness、默认模型、`queue.storage` 和 `egressProxy.connected=true`,`/api/tasks/overview` 返回 PostgreSQL 队列总览,Project Manager `/health` 返回 `storage.primary=postgres` 和项目数量,backend-core `/api/performance` 返回性能指标,`/api/nodes` 中出现 `main-server`、`D601` 和 `D518` provider 且状态为 `online`,`/api/nodes/system-status` 中出现对应 CPU/内存/硬盘采样,`/api/nodes/docker-status` 中能看到主 server、D601 与 D518 Docker 快照。D518 必须通过 K3S agent 加入 K3S 控制面并运行 `code-queue-d518` standby Pod;不得用 D601->D518 直连、NodePort 或 provider-gateway business HTTP 绕过 Kubernetes service route。交付前还必须运行 `bun scripts/cli.ts e2e run`,并以 `docs/reference/e2e.md` 的门禁作为最终判定。 ## Code Queue D601 Resource Budget -Code Queue 已从主 server 迁移到 D601 v3s/k8s,但仍必须保持明确的 memory/swap 硬上限,默认 `CODE_QUEUE_MAX_ACTIVE_QUEUES=0` 以恢复 queue 间并行,仍保持 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10` 这类小热窗口;任务历史、队列统计和 Trace/output 读取必须优先从 PostgreSQL 直读或聚合,`/health` 只做轻量 readiness,不能为了性能便利在 Bun 进程内缓存全量历史。任何提高 Code Queue 热窗口、日志缓冲、Playwright/Codex 子进程常驻规模或容器上限的变更,或把 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 显式改成正数,都必须在同一任务里说明 D601 资源预算来源,并通过 D601 `KUBECONFIG=/home/ubuntu/unidesk-code-queue-deploy/.state/v8s/kubeconfig kubectl -n unidesk get deploy,svc,pod`、`kubectl -n unidesk top pod` 或等价 Docker stats、`microservice health code-queue` 和对应 E2E 证明未重新引入内存爆炸风险。 +Code Queue 已从主 server 迁移到 D601 k3s/k8s,但仍必须保持明确的 memory/swap 硬上限,默认 `CODE_QUEUE_MAX_ACTIVE_QUEUES=0` 以恢复 queue 间并行,仍保持 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10` 这类小热窗口;任务历史、队列统计和 Trace/output 读取必须优先从 PostgreSQL 直读或聚合,`/health` 只做轻量 readiness,不能为了性能便利在 Bun 进程内缓存全量历史。任何提高 Code Queue 热窗口、日志缓冲、Playwright/Codex 子进程常驻规模或容器上限的变更,或把 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 显式改成正数,都必须在同一任务里说明 D601 资源预算来源,并通过 D601 `KUBECONFIG=/home/ubuntu/unidesk-code-queue-deploy/.state/k3s/kubeconfig kubectl -n unidesk get deploy,svc,pod`、`kubectl -n unidesk top pod` 或等价 Docker stats、`microservice health code-queue` 和对应 E2E 证明未重新引入内存爆炸风险。 ## Database Connection Budget diff --git a/docs/reference/e2e.md b/docs/reference/e2e.md index a2cd7f87..8c5f2c26 100644 --- a/docs/reference/e2e.md +++ b/docs/reference/e2e.md @@ -35,13 +35,13 @@ Typical targeted commands: - Core API: `docker exec unidesk-backend-core` calls internal `GET /api/overview`, which must report `dbReady: true`, `pgdata.volumeName=unidesk_pgdata_10gb`, a positive PostgreSQL database byte count, and at least one online node; internal `GET /api/performance` must report component request statistics, internal operation statistics, PGDATA usage and Code Queue PostgreSQL storage metadata. - Provider self-connection: internal `GET /api/nodes` must contain `main-server` with `status: online`, `labels.providerGatewayVersion` equal to `src/components/provider-gateway/package.json`, `labels.providerGatewayUpgradePolicy: "always-enabled"`, `labels.providerGatewayRestartPolicyOk: true`, `labels.providerGatewayPidModeOk: true`, and `labels.providerGatewayRuntimeGuardOk: true`; internal `GET /api/nodes/system-status` must contain CPU/memory/disk samples plus a non-empty process resource list sorted by memory by default; internal `GET /api/nodes/docker-status` must contain a Docker snapshot for `main-server`; every running `provider-gateway` container visible in Docker snapshots must report `restartPolicy: "always"` and `pidMode: "host"`; public provider ingress `/health` must return ok. - Provider remote control: internal `/api/dispatch` must successfully complete a real `provider.upgrade` task in `mode: "plan"` so the upgrade path is validated without recreating the running gateway during E2E. -- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `v3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/v3sctl-adapter/health` and `/api/microservices/v3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-v8s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active instance `servingHealthy=true`, D518 standby instance `healthy=true`, `presentNodeIds=[D601,D518]`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> v3sctl-adapter -> Kubernetes API service proxy -> v3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. +- User services: internal `/api/microservices` must include `todo-note` and `oa-event-flow` on `main-server`, canonical `filebrowser` on `D518`, plus `k3sctl-adapter`, `code-queue`, `findjob`, `pipeline`, `met-nonlinear`, `claudeqq` and `filebrowser-d601` on `D601` with `public=false`; `/api/microservices/todo-note/health` must report `storage=postgres`, `/api/microservices/todo-note/proxy/api/instances` must expose the migrated Todo Note lists, and a temporary Todo Note list create/add/toggle/undo/delete cycle must succeed through the real provider-gateway proxy; `/api/microservices/oa-event-flow/health`, `/api/microservices/oa-event-flow/proxy/api/diagnostics`, `/api/microservices/oa-event-flow/proxy/api/events`, `/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline` and `/api/microservices/oa-event-flow/proxy/api/stats/trace` must prove the independent OA event table、Pipeline bridge 和 stats center are reachable through UniDesk proxy; `/api/microservices/k3sctl-adapter/health` and `/api/microservices/k3sctl-adapter/proxy/api/control-plane` must expose the D601 `unidesk-k3s` control plane, `kubeApiProxy.mode=kubernetes-api-service-proxy`, D601 active instance `servingHealthy=true`, D518 standby instance `healthy=true`, `presentNodeIds=[D601,D518]`, `missingNodeIds=[]`, `status=healthy`, and `noFallback=true`; `/api/microservices/code-queue/health` must return the active Code Queue backend summary with default model `gpt-5.5`, `egressProxy.connected=true`, and `/api/microservices/code-queue/proxy/api/tasks/overview` must return queue state through backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> k3s/k8s Service, not through a `serviceId=code-queue` provider-gateway direct task or `/api/code-queue-direct`; `/api/microservices/filebrowser/health`, `/api/microservices/filebrowser-d601/health` and `/api/microservices/filebrowser/proxy/` must prove File Browser health and WebUI access through UniDesk proxy; `/api/microservices/findjob/health` and `/api/microservices/findjob/proxy/api/summary` must succeed through the real provider-gateway proxy; `/api/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:5` must return a bounded preview with `_unidesk.arrayLimits` metadata; `/api/microservices/pipeline/health`, `/api/microservices/pipeline/proxy/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3` and `/api/microservices/pipeline/proxy/api/oa-event-flow/diagnostics` must return Pipeline health, registry/run previews and OA event-flow evidence; `/api/microservices/met-nonlinear/health`, `/api/microservices/met-nonlinear/proxy/api/queue`, `/api/microservices/met-nonlinear/proxy/api/projects?root=projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects?root=ex_projects&limit=500`, `/api/microservices/met-nonlinear/proxy/api/projects/config?path=` and `/api/microservices/met-nonlinear/proxy/api/images` must return the D601 TS backend health, queue/GPU policy, full project tree inputs, structured project detail and ready `met-nonlinear-ml:tf26` image status. - ClaudeQQ availability: `/api/microservices/claudeqq/health` must only pass when `ready=true`, NapCat HTTP and WebSocket are connected, and `napcat.loginState=logged_in`; `/api/microservices/claudeqq/proxy/api/napcat/login` must show the same logged-in account state and `/api/microservices/claudeqq/proxy/api/events/recent` must prove the backend can read the persistent event cache. A QR-code-only or not-logged-in NapCat state must be treated as unhealthy. - Database: the command writes an `unidesk_e2e_markers` row through `docker exec unidesk-database psql`, confirms provider state is stored in PostgreSQL, and checks Todo Note rows exist in `todo_note_instances` using the same named volume. - Pipeline OA event flow: `microservice:pipeline-oa-event-flow` must prove both no-audit and monitor-audit runs are driven by OA events end to end. The event stream must show `node-finished` as a neutral fact with `pipeline:{pipelineId}` and `epoch:{runId}` tags, OA policy as the source of downstream/audit decisions, monitor decisions as OA control events, and runner control-result evidence. E2E must fail if delivery still depends on a legacy detail audit policy flag as policy authority, independent legacy audit-request points, a legacy batch completion gate, direct monitor-to-runner calls, or frontend/CLI writes to Pipeline `.state`. - The same Pipeline OA diagnostics must fail on legacy file-transport residuals. Procedure containers, monitor sessions, UI/Gantt DTO builders and CLI fetches must consume prompt/control/stop/display evidence only from the OA event ledger and normalized HTTP read APIs; `control-prompts.jsonl`, `monitor-prompts.jsonl`, `monitor-control`, `control-events.jsonl`, monitor stop files, `.state/pipeline-runs/{runId}/control/commands/`, `PIPELINE_*_APPEND_FILE`, local JSONL append/read helpers, and monitor `/pipeline-state` mounts are forbidden in runtime source. - Pipeline live Gantt setup: when `frontend:pipeline-gantt-observation-live-running` is selected, E2E first looks for a current Pipeline run that already contains both a `node-long-running-observation` marker and a still-running execution interval. If no such candidate exists, the E2E setup starts the D601 `monitor-management-behavior-test` pipeline through `bun scripts/cli.ts ssh D601 ...` and polls the private backend proxy until the observation candidate exists; the acceptance assertion itself still opens the public frontend with Playwright and verifies the rendered arrows, absence of observation source pseudo-points, target arrow inset, and live flashing running bar through React DOM controls. -- Frontend: Playwright must open the public frontend URL derived from `network.publicHost`, not localhost or a Docker-internal URL; it logs in with the configured account, waits for `核心在线`, asserts that `main-server` and `Main Server Provider` are visible, verifies desktop sidebar collapse and `PGDATA` overview metric, opens `运行总览 / 性能面板` to verify `Bwebui`、组件汇总、最近失败请求、内部操作汇总和最近慢操作, clicks `查看原始JSON` to verify Provider data from the frontend, confirms no raw JSON is visible before that click, opens task history to verify duration and failure diagnostics, opens resource nodes `资源监控` to verify CPU/Memory/Disk curves, the structured process resource table, default memory-desc sorting, sortable CPU column and provider upgrade precheck dispatch, opens `Docker 状态`, switches to `main-server`, and verifies the Docker Desktop-style container view including the database named volume `unidesk_pgdata_10gb`, opens `网关版本` and verifies the provider-gateway version, SSH 透传可用性、远程更新可用性 plus structured remote update records for `provider.upgrade`, then opens `用户服务 / 服务目录`、`用户服务 / Todo Note`、`用户服务 / OA Event Flow`、`用户服务 / V3S Control`、`用户服务 / Code Queue`、`用户服务 / FindJob`、`用户服务 / Pipeline` and `用户服务 / MET Nonlinear` to verify 主 server Todo Note/OA Event Flow、D601 Code Queue、D601 业务服务、仓库引用、私有后端映射、Todo Note 迁移清单和树形任务、OA Event Flow 事件表和 Trace stats 表、V3S 控制面/D601-D518 实例/Kubernetes API service proxy/no-fallback 路径、Code Queue 队列/模型/输出/初始 `Submitted prompt`/终态任务自动加载完整 Trace/追加 prompt/打断控件、FindJob 指标和岗位预览、Pipeline 组件矩阵、MiniMax 限额卡片、结构化 OA 事件流诊断面板、React Flow 控制图、epoch 甘特图、甘特图渲染图导出、monitor 首列排序、长任务观察连线、无观察来源伪点、running node 实时闪动执行条和 OpenCode Trace、MET Nonlinear 项目库/Fork/待启动队列/当前队列/已完成/失败诊断/GPU/镜像都通过 React 控件展示。Playwright 还必须验证 Code Queue 页面所有 API 请求走 `/api/microservices/code-queue/proxy`,不得再出现 `/api/code-queue-direct`;深链接直达路由例如公网 `http://:/app/pipeline/` 能直接落到 Pipeline 页面,随后切到 `资源节点 / Docker 状态` 时地址栏更新为 `/nodes/docker/`,并且浏览器 history 返回链路仍能回到 `/app/pipeline/`;还必须直开 `/app/code-queue/` 验证页面存在 `app-shell`、左侧主模块边栏、顶部状态栏、顶部子标签和 `code-queue-page`,防止用户服务 deep link 退化成缺 shell 的 standalone 页面;同时 `态势总览` 这类非用户服务页面应落在自己的模块前缀下,例如 `/ops/status/`。Playwright 必须覆盖默认可见时间按北京时间显示,至少包括顶部 `北京时间` 时钟、任务历史/网关版本更新时间和用户服务刷新时间,不得随浏览器本地时区漂移。Task history and provider upgrade records must not display a real sub-second duration as `0s`; MET Nonlinear running rows must show an ETA derived from backend progress or from `startedAt` plus epoch progress, and queue/completed rows must show training speed as `epoch/h`. +- Frontend: Playwright must open the public frontend URL derived from `network.publicHost`, not localhost or a Docker-internal URL; it logs in with the configured account, waits for `核心在线`, asserts that `main-server` and `Main Server Provider` are visible, verifies desktop sidebar collapse and `PGDATA` overview metric, opens `运行总览 / 性能面板` to verify `Bwebui`、组件汇总、最近失败请求、内部操作汇总和最近慢操作, clicks `查看原始JSON` to verify Provider data from the frontend, confirms no raw JSON is visible before that click, opens task history to verify duration and failure diagnostics, opens resource nodes `资源监控` to verify CPU/Memory/Disk curves, the structured process resource table, default memory-desc sorting, sortable CPU column and provider upgrade precheck dispatch, opens `Docker 状态`, switches to `main-server`, and verifies the Docker Desktop-style container view including the database named volume `unidesk_pgdata_10gb`, opens `网关版本` and verifies the provider-gateway version, SSH 透传可用性、远程更新可用性 plus structured remote update records for `provider.upgrade`, then opens `用户服务 / 服务目录`、`用户服务 / Todo Note`、`用户服务 / OA Event Flow`、`用户服务 / K3S Control`、`用户服务 / Code Queue`、`用户服务 / FindJob`、`用户服务 / Pipeline` and `用户服务 / MET Nonlinear` to verify 主 server Todo Note/OA Event Flow、D601 Code Queue、D601 业务服务、仓库引用、私有后端映射、Todo Note 迁移清单和树形任务、OA Event Flow 事件表和 Trace stats 表、K3S 控制面/D601-D518 实例/Kubernetes API service proxy/no-fallback 路径、Code Queue 队列/模型/输出/初始 `Submitted prompt`/终态任务自动加载完整 Trace/追加 prompt/打断控件、FindJob 指标和岗位预览、Pipeline 组件矩阵、MiniMax 限额卡片、结构化 OA 事件流诊断面板、React Flow 控制图、epoch 甘特图、甘特图渲染图导出、monitor 首列排序、长任务观察连线、无观察来源伪点、running node 实时闪动执行条和 OpenCode Trace、MET Nonlinear 项目库/Fork/待启动队列/当前队列/已完成/失败诊断/GPU/镜像都通过 React 控件展示。Playwright 还必须验证 Code Queue 页面所有 API 请求走 `/api/microservices/code-queue/proxy`,不得再出现 `/api/code-queue-direct`;深链接直达路由例如公网 `http://:/app/pipeline/` 能直接落到 Pipeline 页面,随后切到 `资源节点 / Docker 状态` 时地址栏更新为 `/nodes/docker/`,并且浏览器 history 返回链路仍能回到 `/app/pipeline/`;还必须直开 `/app/code-queue/` 验证页面存在 `app-shell`、左侧主模块边栏、顶部状态栏、顶部子标签和 `code-queue-page`,防止用户服务 deep link 退化成缺 shell 的 standalone 页面;同时 `态势总览` 这类非用户服务页面应落在自己的模块前缀下,例如 `/ops/status/`。Playwright 必须覆盖默认可见时间按北京时间显示,至少包括顶部 `北京时间` 时钟、任务历史/网关版本更新时间和用户服务刷新时间,不得随浏览器本地时区漂移。Task history and provider upgrade records must not display a real sub-second duration as `0s`; MET Nonlinear running rows must show an ETA derived from backend progress or from `startedAt` plus epoch progress, and queue/completed rows must show training speed as `epoch/h`. - Frontend dense-layout regression gate: whenever a frontend change touches Pipeline 右侧边栏、Trace timeline、详情抽屉、甘特图坐标或其他高信息密度面板, Playwright acceptance must inspect both `总高度` and `横向滚动条`. For Pipeline specifically, the OpenCode Trace session head must carry shared agent/model/session facts and the Trace body must use the same Code Queue `TraceView` styling; Playwright must fail if old `.pipeline-opencode-step`, `.pipeline-opencode-flow`, `.pipeline-step-message-card` or `.pipeline-opencode-part` user-visible styles reappear, if the Trace container introduces an internal horizontal scrollbar, or if `frontend:pipeline-gantt-frontend-y-accuracy` fails to prove the frontend `frontend-y` layout maps ticks, markers and execution bars from timestamps to y coordinates within tolerance. - OpenCode Trace must use Code Queue Trace styling and must not render the deprecated Pipeline continuous step connector; Playwright should fail if `.pipeline-opencode-flow`, `.pipeline-opencode-step` or any equivalent continuous connector/card returns to the user-visible Trace. - User service frontend assertions must wait for real backend data, not only the page skeleton. For Todo Note this means the page must show the migrated lists `CONSTAR`、`大论文`、`找工作`、`小论文`、`事务`, support creating a temporary list and task through the frontend, and delete that temporary list afterwards. The temporary list must be selected again by its unique generated name before deletion so E2E never deletes a migrated source list by accident. For FindJob this means the page must show a numeric `岗位总量`, `HEALTH OK`, and a non-empty `PREVIEW` count such as `40/1463 PREVIEW`; for Pipeline this means the page must show `Pipeline v2 工作台`, `Health OK`, a numeric component count, a non-empty React Flow control graph, `控制图`, `Epoch 甘特图`, and after clicking a Gantt execution line it must show `OpenCode Trace` rendered by the shared Code Queue-style Trace component with messages and tool-call groups; for MET Nonlinear this means the page must show `MET Nonlinear 训练编排`, `Health OK`, `Fork Project`, `加入待启动队列`, `启动队列`, `当前队列`, 最大并发设置、task queue and GPU/image panels, and must not show the removed hard-coded `创建10个10轮任务` frontend entry. The MET Nonlinear project library must render `projects/` and `ex_projects/` as a true path tree with folder Project counts; clicking a project row must open a structured detail panel containing `config.json`, `data/ 训练状态`, `模型参数`, `指标` and a parameter count such as `Total Params`; clicking a completed/current/failed job row must open a structured job detail and both the row and detail must show `epoch/h`. Full MET Nonlinear acceptance is driven by public frontend controls: choose a visible source Project, set batch size, epochs and max concurrency in inputs, fork into `projects/unidesk_forks/`, stage the selected forks, start the queue, and verify completed rows plus automatic `metnl-train-*` container removal; loading placeholders like `--` or empty states are not sufficient for E2E success. @@ -67,7 +67,7 @@ The PostgreSQL data volume is the named Docker volume `unidesk_pgdata_10gb`. CLI ## User Service Restart-Recovery Rule -Any new user service, service migration, or change to a service's Compose/docker run/k8s configuration must prove it can recover after container restart and Docker daemon restart. The delivery evidence must include the service's `config.json` id/provider/container or Kubernetes Service mapping, restart policy or Deployment replica policy, private port or ClusterIP Service, persistent mounts or PostgreSQL tables, health readiness fields, and at least one post-restart `bun scripts/cli.ts microservice health ` plus a representative `microservice proxy` check through the real UniDesk path. `v3sctl-managed` services must prove the proxy path through `v3sctl-adapter` and Kubernetes API service proxy, not the provider-gateway direct business path. +Any new user service, service migration, or change to a service's Compose/docker run/k8s configuration must prove it can recover after container restart and Docker daemon restart. The delivery evidence must include the service's `config.json` id/provider/container or Kubernetes Service mapping, restart policy or Deployment replica policy, private port or ClusterIP Service, persistent mounts or PostgreSQL tables, health readiness fields, and at least one post-restart `bun scripts/cli.ts microservice health ` plus a representative `microservice proxy` check through the real UniDesk path. `k3sctl-managed` services must prove the proxy path through `k3sctl-adapter` and Kubernetes API service proxy, not the provider-gateway direct business path. D601 services have an extra gate because Windows, WSL and Docker Desktop are separate supervisors: record the Windows scheduled task or equivalent keepalive, run `docker inspect` to confirm `met-nonlinear-ts`, `claudeqq-backend`, `claudeqq-napcat` and any changed service have non-empty restart policies and host bind mounts for durable state, then verify MET Nonlinear queue/image health and ClaudeQQ logged-in NapCat HTTP/WebSocket state after the restart. A service that only becomes `running` but loses login, queue, token, subscription, data directory or pending work is not restart-recovery complete. diff --git a/docs/reference/frontend.md b/docs/reference/frontend.md index 76ac8b6f..5dd80442 100644 --- a/docs/reference/frontend.md +++ b/docs/reference/frontend.md @@ -6,7 +6,7 @@ UniDesk 前端是 React 组件化工业控制台,不追求展示型大屏效 frontend 应用源码必须使用 TypeScript + React,禁止在 `src/components/frontend` 中维护手写 `.js` / `.jsx` 应用源码。浏览器请求的 `/app.js` 只能由 frontend Bun server 从 `src/components/frontend/src/app.tsx` 及其 TSX imports 转译生成;`public/` 目录只保存 HTML/CSS 等静态资产,不提交手写 `app.js`。 -`src/components/frontend/src/app.tsx` 只承担应用 shell、登录、全局数据加载、主模块/子标签路由和通用控制台页面。用户服务前端必须模块化到独立 TSX 文件,禁止继续把所有业务页面堆进 `app.tsx`。当前长期固定入口为:`todo-note.tsx` 承载 Todo Note 工作台,`findjob.tsx` 承载 FindJob 工作台,`pipeline.tsx` 承载 Pipeline 工作台,`met-nonlinear.tsx` 承载 MET Nonlinear 训练编排工作台,`claudeqq.tsx` 承载 ClaudeQQ QQ 消息网关工作台,`baidu-netdisk.tsx` 承载 Baidu Netdisk 存储工作台,`code-queue.tsx` 承载 Code Queue 控制台,`oa-event-flow.tsx` 承载统一 OA 事件流控制台,`v3sctl.tsx` 承载 V3S Control Plane 页面;新增用户服务也必须按同样规则新增独立页面模块,并由 `app.tsx` 只做导入和路由分发。 +`src/components/frontend/src/app.tsx` 只承担应用 shell、登录、全局数据加载、主模块/子标签路由和通用控制台页面。用户服务前端必须模块化到独立 TSX 文件,禁止继续把所有业务页面堆进 `app.tsx`。当前长期固定入口为:`todo-note.tsx` 承载 Todo Note 工作台,`findjob.tsx` 承载 FindJob 工作台,`pipeline.tsx` 承载 Pipeline 工作台,`met-nonlinear.tsx` 承载 MET Nonlinear 训练编排工作台,`claudeqq.tsx` 承载 ClaudeQQ QQ 消息网关工作台,`baidu-netdisk.tsx` 承载 Baidu Netdisk 存储工作台,`code-queue.tsx` 承载 Code Queue 控制台,`oa-event-flow.tsx` 承载统一 OA 事件流控制台,`k3sctl.tsx` 承载 K3S Control Plane 页面;新增用户服务也必须按同样规则新增独立页面模块,并由 `app.tsx` 只做导入和路由分发。 ## Shared Dialog Component @@ -95,9 +95,9 @@ frontend shell 必须把左侧主模块与顶部子标签编译为统一的 URL - `ClaudeQQ` 子标签必须把 D601 ClaudeQQ 后端渲染为 UniDesk React 控件,包括 NapCat 容器登录二维码、NapCat HTTP/WS 状态、事件缓存、QQ 事件订阅表、订阅创建表单、消息推送表单、主用户私聊账号 `645275593` 标记、最近 QQ 事件、已发送记录和显式原始 JSON 按钮。 - `Baidu Netdisk` 子标签必须把主 server `baidu-netdisk-backend` 后端渲染为 UniDesk React 控件,包括 OAuth 设备码二维码/用户码登录、账号容量、配置工作根文件浏览(当前默认百度网盘根目录 `/`)、staging 目录上传/下载任务、上传/下载自测按钮与 MD5 结果、脱敏安全说明、日志摘要和显式原始 JSON 按钮;不得把 access token、refresh token、dlink 或 staging 文件字节流裸露到浏览器。 - `OA Event Flow` 子标签必须把主 server `oa-event-flow-backend` 后端渲染为 UniDesk React 控件,包括服务健康、事件表、tag 过滤、SSE live 状态、Trace/STEP stats 表、Code Queue/Pipeline 标签入口和显式原始 JSON 按钮;默认页面不得裸铺完整事件 JSON,事件表只展示结构化摘要,完整 envelope/payload 只能通过 `查看原始JSON` 打开。 - - `V3S Control` 子标签必须把 D601 `v3sctl-adapter` 控制面渲染为 UniDesk React 控件,包括 control plane 状态、manifest 列表、D601/D518 节点实例、active instance、single-writer/no-fallback 路径、Kubernetes API service proxy 状态、kubectl/v3s snapshot 摘要和显式原始 JSON 按钮;页面只能通过 `/api/microservices/v3sctl-adapter/proxy/api/control-plane` 取数,不得直接访问 provider-gateway、NodePort、业务容器端口或裸 v3s/kubectl API。 - - `Code Queue` 子标签必须把 D601 v3s/k8s `code-queue` Service 渲染为 UniDesk React 控件,前端 API 基址只能是 `/api/microservices/code-queue/proxy`,不能继续使用旧 `/api/code-queue-direct` 别名;页面包括多 queue lane、queue 内串行、queue 间并行、queue 合并(点击“合并 queue”后必须用公共 `UniDeskDialog` 打开独立小窗口,用下拉菜单选择源 queue;不得把源 queue 选择控件塞进正常提交任务的 Queue 选择区;合并后自动删除源 queue,只保留合并后的目标 queue,目标 queue 按原 queueEnteredAt/createdAt 时间顺序串行)、任务 ID/复制任务 ID、引用按钮、任务耗时、任务提交/批量提交、引用任务 ID、创建成功提示、清空输入、模型下拉、执行 Provider 下拉、执行模式下拉(默认容器/本机或 `windows-native`)、显式入队份数、默认模型 `gpt-5.5`、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、运行中追加 prompt、打断、手动重试和显式原始 JSON 按钮;`windows-native` 模式必须在任务 JSON、卡片和 Trace 头部显示,并要求非本机 WSL Provider 与 `/mnt/` 工作目录;Codex CLI-like 输出流必须始终保留任务的初始 `Submitted prompt` 和运行中 `Steer prompt`;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call,但非错误 system event 默认只保留在原始输出/数据库中,不在 TraceView 展示;Code Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;Trace 首屏可以是摘要预览,但终态任务被选中后必须自动在后台加载完整 Trace,手动“加载完整 Trace”也必须从 Code Queue output archive 分页补齐早期 trace,不得把 preview 的 `hasMore=false` 当成完整历史;即使热状态为控制体积裁剪了早期 raw output,也要从结构化 `basePrompt/displayPrompt/promptHistory` 和 archive 合成完整用户输入与 agent trace,并且初始 prompt 默认显示注入前 prompt 而不是引用注入全文;当初始 prompt 含引用注入时,引用内容必须默认折叠,并只在 Trace 的初始消息中提供可展开的“最终传入 Codex 的真实完整 prompt”,不得再渲染独立 Prompt 全量卡片;多轮引用注入必须按上游/最早上下文在前、直接引用在后的顺序排列,每一轮必须有明确 `Reference Round N/M` 分割线和时间范围,不能用固定 6 轮截断引用链;点击队列引用按钮必须自动把该任务 ID 写入提交表单的引用输入框,引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示;连续执行同一 prompt 应通过入队份数一次性生成多条任务,避免快速连点造成操作员误判。 - - `MDTODO` 子标签必须把 D601 k3s/v3s `mdtodo` Service 渲染为 UniDesk React 控件,前端 API 基址只能是 `/api/microservices/mdtodo/proxy`;页面包括 TODO Markdown 文件列表、任务树、状态徽标、标题与正文编辑、新增根任务/子任务、删除任务、执行命令生成、hostPath 健康摘要和显式原始 JSON 按钮,不得 iframe 原 VS Code webview、公开 VSIX 旧前端或把完整 Markdown/JSON 默认铺在页面上。 + - `K3S Control` 子标签必须把 D601 `k3sctl-adapter` 控制面渲染为 UniDesk React 控件,包括 control plane 状态、manifest 列表、D601/D518 节点实例、active instance、single-writer/no-fallback 路径、Kubernetes API service proxy 状态、kubectl/k3s snapshot 摘要和显式原始 JSON 按钮;页面只能通过 `/api/microservices/k3sctl-adapter/proxy/api/control-plane` 取数,不得直接访问 provider-gateway、NodePort、业务容器端口或裸 k3s/kubectl API。 + - `Code Queue` 子标签必须把 D601 k3s/k8s `code-queue` Service 渲染为 UniDesk React 控件,前端 API 基址只能是 `/api/microservices/code-queue/proxy`,不能继续使用旧 `/api/code-queue-direct` 别名;页面包括多 queue lane、queue 内串行、queue 间并行、queue 合并(点击“合并 queue”后必须用公共 `UniDeskDialog` 打开独立小窗口,用下拉菜单选择源 queue;不得把源 queue 选择控件塞进正常提交任务的 Queue 选择区;合并后自动删除源 queue,只保留合并后的目标 queue,目标 queue 按原 queueEnteredAt/createdAt 时间顺序串行)、任务 ID/复制任务 ID、引用按钮、任务耗时、任务提交/批量提交、引用任务 ID、创建成功提示、清空输入、模型下拉、执行 Provider 下拉、执行模式下拉(默认容器/本机或 `windows-native`)、显式入队份数、默认模型 `gpt-5.5`、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、运行中追加 prompt、打断、手动重试和显式原始 JSON 按钮;`windows-native` 模式必须在任务 JSON、卡片和 Trace 头部显示,并要求非本机 WSL Provider 与 `/mnt/` 工作目录;Codex CLI-like 输出流必须始终保留任务的初始 `Submitted prompt` 和运行中 `Steer prompt`;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call,但非错误 system event 默认只保留在原始输出/数据库中,不在 TraceView 展示;Code Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;Trace 首屏可以是摘要预览,但终态任务被选中后必须自动在后台加载完整 Trace,手动“加载完整 Trace”也必须从 Code Queue output archive 分页补齐早期 trace,不得把 preview 的 `hasMore=false` 当成完整历史;即使热状态为控制体积裁剪了早期 raw output,也要从结构化 `basePrompt/displayPrompt/promptHistory` 和 archive 合成完整用户输入与 agent trace,并且初始 prompt 默认显示注入前 prompt 而不是引用注入全文;当初始 prompt 含引用注入时,引用内容必须默认折叠,并只在 Trace 的初始消息中提供可展开的“最终传入 Codex 的真实完整 prompt”,不得再渲染独立 Prompt 全量卡片;多轮引用注入必须按上游/最早上下文在前、直接引用在后的顺序排列,每一轮必须有明确 `Reference Round N/M` 分割线和时间范围,不能用固定 6 轮截断引用链;点击队列引用按钮必须自动把该任务 ID 写入提交表单的引用输入框,引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示;连续执行同一 prompt 应通过入队份数一次性生成多条任务,避免快速连点造成操作员误判。 + - `MDTODO` 子标签必须把 D601 k3s `mdtodo` Service 渲染为 UniDesk React 控件,前端 API 基址只能是 `/api/microservices/mdtodo/proxy`;页面包括 TODO Markdown 文件列表、任务树、状态徽标、标题与正文编辑、新增根任务/子任务、删除任务、执行命令生成、hostPath 健康摘要和显式原始 JSON 按钮,不得 iframe 原 VS Code webview、公开 VSIX 旧前端或把完整 Markdown/JSON 默认铺在页面上。 - `Code Queue` 前端改进必须在同一任务内重建并上线公网 frontend,不能只修改源码或本地 bundle;重建 frontend 是无状态 WebUI 替换,不会导致 Code Queue 长期任务失败。已结束未读任务只能在 task card 边角显示类似未读消息的 `codex-unread-badge` 圆点和“标为已读”操作,不得把整张卡片改成红色/琥珀色失败态边框、背景或胶囊标签;状态栏的“结束未读”提示也不得使用失败态红色。 - `Code Queue` 前端必须把 PostgreSQL-backed backend API 作为 task、queue、readAt/未读状态和 attempt 状态的唯一数据来源;不得用 `localStorage`、`sessionStorage` 或 IndexedDB 持久化这些业务状态,也不得在后端标记已读失败时伪造本地成功。前端允许保留 React 内存态、请求 in-flight guard 和本轮页面缓存,但刷新页面或切换设备后的状态必须完全由后端 PostgreSQL 数据恢复。 - `Code Queue` 前后端通信必须采用渐进式披露:首屏只请求 queue/task 轻量摘要、必要的 selected preview 和小体积统计,不得默认拉取完整 transcript、raw output、原始 JSON 或全部历史任务;加载下一页或搜索分页时必须显式传递 `selected=0`、`includeActive=0`、`stats=0` 等价开关,避免每一页重复请求 selected/active/stats;点击/选中 task 后再按需加载 summary、prompt part、trace step、raw output 或完整 Trace。`read`/`mark all read` 应调用专用 mutation 并用后端返回的 patch 更新当前内存态,不能为了隐藏未读圆点而强制刷新完整 overview;请求仍需遵守 PostgreSQL 权威源,失败时不得本地伪造已读。Code Queue 性能问题应优先通过缩小 API 响应、分页/cursor、去重 in-flight 请求、短 TTL 且 mutation 失效的页面缓存和后端 SQL 聚合解决,避免以重写渲染层或把大 JSON 藏在 DOM/React state 中规避慢请求。 diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index a1337a1f..62e5e5fa 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -7,9 +7,9 @@ UniDesk 用户服务是挂载到 UniDesk 核心服务上的、面向用户使用 ## Boundary - 用户服务后端端口默认只绑定计算节点本机地址,例如 `127.0.0.1:`,不得直接暴露公网。 -- 浏览器只访问 UniDesk frontend;frontend 通过同源 `/api/microservices/*` 代理到 backend-core。`deployment.mode=unidesk-direct` 的用户服务由 backend-core 通过目标 provider-gateway 的 `microservice.http` 能力访问计算节点本机后端;`deployment.mode=v3sctl-managed` 的用户服务只允许经 `v3sctl-adapter` 微服务进入 v3s 标准服务路由,backend-core 不得直接向业务容器所在 provider 下发 `microservice.http`。 +- 浏览器只访问 UniDesk frontend;frontend 通过同源 `/api/microservices/*` 代理到 backend-core。`deployment.mode=unidesk-direct` 的用户服务由 backend-core 通过目标 provider-gateway 的 `microservice.http` 能力访问计算节点本机后端;`deployment.mode=k3sctl-managed` 的用户服务只允许经 `k3sctl-adapter` 微服务进入 k3s 标准服务路由,backend-core 不得直接向业务容器所在 provider 下发 `microservice.http`。 - backend-core REST API、database 和计算节点用户服务后端都不得新增公网端口;公网入口仍只有 frontend 和 provider ingress。 -- `microservice.http` 只允许 provider-gateway 访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这类节点本地地址,或明确登记为同一私有 Docker network 内的服务名;主 server 内置用户服务可使用同一 Compose 网络内的显式服务名,例如 `todo-note:4211`。v3s 代管服务不得把业务容器地址登记成 provider-gateway 直连目标,`backend.proxyMode` 必须使用 `v3sctl-adapter-http`,`backend.nodeBaseUrl` 可使用 `v3s://` 这类逻辑服务名。backend-core 还必须用 `allowedPathPrefixes` 和 `allowedMethods` 同时限制可代理路径和 HTTP 方法。 +- `microservice.http` 只允许 provider-gateway 访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这类节点本地地址,或明确登记为同一私有 Docker network 内的服务名;主 server 内置用户服务可使用同一 Compose 网络内的显式服务名,例如 `todo-note:4211`。k3s 代管服务不得把业务容器地址登记成 provider-gateway 直连目标,`backend.proxyMode` 必须使用 `k3sctl-adapter-http`,`backend.nodeBaseUrl` 可使用 `k3s://` 这类逻辑服务名。backend-core 还必须用 `allowedPathPrefixes` 和 `allowedMethods` 同时限制可代理路径和 HTTP 方法。 ## Config Contract @@ -19,7 +19,7 @@ UniDesk 用户服务是挂载到 UniDesk 核心服务上的、面向用户使用 - `repository.url` 与 `repository.commitId`,用于记录业务代码的外部权威来源;UniDesk 不 vendoring 业务全量代码。 - `repository.dockerfile`、`repository.composeFile`、`repository.composeService` 和 `repository.containerName`,用于说明部署应复用业务仓库自身维护的 Dockerfile/docker-compose。 - `backend.nodeBaseUrl`、`backend.nodeBindHost`、`backend.nodePort`、`backend.proxyMode`、`backend.public=false`、`backend.frontendOnly=true`、`backend.allowedMethods`、`backend.allowedPathPrefixes` 和 `backend.healthPath`,用于定义计算节点端口到 UniDesk frontend-only 代理的映射。 -- `deployment.mode`,用于明确部署责任边界;`unidesk-direct` 表示 UniDesk 直接登记和探测目标 provider 上的容器,`v3sctl-managed` 表示 UniDesk 只登记逻辑服务并经 `deployment.adapterServiceId` 指向的 `v3sctl-adapter` 访问,代管条目还必须写明 `v3sServiceId`、`namespace`、`expectedNodeIds` 和当前 `activeNodeId`。 +- `deployment.mode`,用于明确部署责任边界;`unidesk-direct` 表示 UniDesk 直接登记和探测目标 provider 上的容器,`k3sctl-managed` 表示 UniDesk 只登记逻辑服务并经 `deployment.adapterServiceId` 指向的 `k3sctl-adapter` 访问,代管条目还必须写明 `k3sServiceId`、`namespace`、`expectedNodeIds` 和当前 `activeNodeId`。 - `development.providerId`、`development.sshPassthrough=true` 和 `development.worktreePath`,用于说明开发调试入口必须在计算节点上通过 UniDesk SSH 透传完成。 - `frontend.route` 和 `frontend.integrated=true`,用于说明该业务前端已经整合到 UniDesk React 控制台,而不是继续公开业务自身前端。 @@ -123,37 +123,37 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度 - provider-gateway 容器必须配置 `extra_hosts: ["host.docker.internal:host-gateway"]`,确保 D601/D518 Linux Docker 容器内能访问 WSL host 侧 `127.0.0.1:4251` 映射。 - UniDesk 前端:`用户服务 / File Browser` React 页面展示 D518 主目标和 D601 备用目标的健康状态、仓库引用、私有后端映射,并以 iframe 嵌入对应 File Browser WebUI;页面必须提供截图导出入口,并对上游 WebUI 注入紧凑布局样式,避免 material icon 字体异常时 `folder` 文本遮挡文件名;File Browser 自身端口不得直接暴露公网。 -### V3S Control Plane Adapter On D601 +### K3S Control Plane Adapter On D601 -`v3sctl-adapter` 是 UniDesk 直管的控制面适配微服务;它本身仍按 `deployment.mode=unidesk-direct` 登记在 D601,由 UniDesk 负责健康检查、重建和前端可见性,但它管理的业务服务必须走 v3s 标准服务路由,不得再被 backend-core 直接下发到业务容器所在 provider。 +`k3sctl-adapter` 是 UniDesk 直管的控制面适配微服务;它本身仍按 `deployment.mode=unidesk-direct` 登记在 D601,由 UniDesk 负责健康检查、重建和前端可见性,但它管理的业务服务必须走 k3s 标准服务路由,不得再被 backend-core 直接下发到业务容器所在 provider。 -- Provider:`D601`,由 D601 provider-gateway 仅维护和访问 `v3sctl-adapter` 的本机私有端口 `127.0.0.1:4266`;provider-gateway 不再作为 `code-queue` 业务请求的直接代理。 -- 代码引用:`https://github.com/pikasTech/unidesk` 与配置中的 `repository.commitId`;服务源码位于 `src/components/microservices/v3sctl-adapter`,属于 UniDesk 自有控制面组件。 -- 部署引用:UniDesk 仓库中的 `src/components/microservices/v3sctl-adapter/docker-compose.d601.yml`,Dockerfile 为 `src/components/microservices/v3sctl-adapter/Dockerfile`,容器名为 `v3sctl-adapter`。 -- v3s 实现:当前运行控制面为 D601 上的 `unidesk-v8s` K3S server 和 D518 上的 K3S agent;`v3sctl-managed` 可以落到 k3s、k8s 或等价标准 Kubernetes 控制面,但必须使用 Kubernetes 原生命名空间、Deployment、Service、readiness/liveness probe、Kubernetes API service proxy 等规范对象;不得把裸容器端口、NodePort、SSH curl、provider-gateway `microservice.http` 或 host 直连地址伪装成 v3s 服务路由。 -- V8S 系统组件:`unidesk-v8s` server 必须禁用非必要的 `traefik`、`servicelb` 和 `metrics-server`,只保留业务必需的 API server、CoreDNS 与 local-path provisioner;CoreDNS 和 local-path provisioner 固定运行在 D601 控制面节点,避免 D518 维护隧道限制导致系统 DNS/readiness 抖动。 -- manifest:代管服务声明放在 `src/components/microservices/v3sctl-adapter/v3s/*.v3s.json`,adapter 启动时通过 `V3SCTL_MANIFEST_PATHS` 读取;manifest 是 D601/D518 实例、active instance、single writer、expected nodes 和 health policy 的权威来源。`V3SCTL_SERVICES_JSON` 不得承载 static HTTP 服务、不得覆盖同名服务、不得作为隐藏 fallback;如需追加服务也必须提供完整 `ManagedKubernetesService` manifest。 -- API:`GET /health` 只表示 adapter 控制面自身可用,并把代管服务 serving 健康作为 `managedServicesHealthy` 字段展示;`GET /api/control-plane` 返回控制面、manifest、kubectl/v3s snapshot 和代管服务状态;`GET /api/services` 返回代管服务列表;`GET|HEAD /api/services//health` 返回该 v3s 服务的 active serving 健康;`/api/services//proxy/*` 是业务请求进入 active service 的唯一代理入口。 +- Provider:`D601`,由 D601 provider-gateway 仅维护和访问 `k3sctl-adapter` 的本机私有端口 `127.0.0.1:4266`;provider-gateway 不再作为 `code-queue` 业务请求的直接代理。 +- 代码引用:`https://github.com/pikasTech/unidesk` 与配置中的 `repository.commitId`;服务源码位于 `src/components/microservices/k3sctl-adapter`,属于 UniDesk 自有控制面组件。 +- 部署引用:UniDesk 仓库中的 `src/components/microservices/k3sctl-adapter/docker-compose.d601.yml`,Dockerfile 为 `src/components/microservices/k3sctl-adapter/Dockerfile`,容器名为 `k3sctl-adapter`。 +- k3s 实现:当前运行控制面为 D601 上的 `unidesk-k3s` K3S server 和 D518 上的 K3S agent;`k3sctl-managed` 可以落到 k3s、k8s 或等价标准 Kubernetes 控制面,但必须使用 Kubernetes 原生命名空间、Deployment、Service、readiness/liveness probe、Kubernetes API service proxy 等规范对象;不得把裸容器端口、NodePort、SSH curl、provider-gateway `microservice.http` 或 host 直连地址伪装成 k3s 服务路由。 +- K3S 系统组件:`unidesk-k3s` server 必须禁用非必要的 `traefik`、`servicelb` 和 `metrics-server`,只保留业务必需的 API server、CoreDNS 与 local-path provisioner;CoreDNS 和 local-path provisioner 固定运行在 D601 控制面节点,避免 D518 维护隧道限制导致系统 DNS/readiness 抖动。 +- manifest:代管服务声明放在 `src/components/microservices/k3sctl-adapter/k3s/*.k3s.json`,adapter 启动时通过 `K3SCTL_MANIFEST_PATHS` 读取;manifest 是 D601/D518 实例、active instance、single writer、expected nodes 和 health policy 的权威来源。`K3SCTL_SERVICES_JSON` 不得承载 static HTTP 服务、不得覆盖同名服务、不得作为隐藏 fallback;如需追加服务也必须提供完整 `ManagedKubernetesService` manifest。 +- API:`GET /health` 只表示 adapter 控制面自身可用,并把代管服务 serving 健康作为 `managedServicesHealthy` 字段展示;`GET /api/control-plane` 返回控制面、manifest、kubectl/k3s snapshot 和代管服务状态;`GET /api/services` 返回代管服务列表;`GET|HEAD /api/services//health` 返回该 k3s 服务的 active serving 健康;`/api/services//proxy/*` 是业务请求进入 active service 的唯一代理入口。 - 代理路径:adapter 访问 active 业务服务的唯一正式路径是 Kubernetes API service proxy:`/api/v1/namespaces//services/:/proxy/...`。D601 与 D518 不要求能彼此直连;D518 通过 K3S agent 加入控制面,控制面连接可以借助节点维护隧道建立,但业务请求不得退化为 provider-gateway 直连 Code Queue HTTP 端口。standby/worker 节点如果受 kubelet/service-proxy 可达性限制,可以在 manifest 中显式使用 `healthMode=pod-ready` 作为拓扑健康探针;这只读取 Kubernetes Pod readiness,不是业务代理路径,也不能替代 active Service proxy。 - 拓扑健康:`expectedNodeIds` 负责展示计划内节点;当前 Code Queue 目标拓扑必须同时包含 D601 和 D518,`presentNodeIds` 应为 `["D601","D518"]`、`missingNodeIds=[]`、`topologyComplete=true`、`status=healthy`。D518 未加入只允许作为迁移中的显式 degraded 状态,不能隐藏为 fallback;只有显式 `requireAllInstancesHealthy=true` 的服务才允许把缺失 standby/worker 节点提升为整体不健康。 -- 前端:`用户服务 / V3S Control` React 页面必须只通过 `/api/microservices/v3sctl-adapter/proxy/api/control-plane` 通信,展示控制面状态、manifest、D601/D518 实例、active instance、Kubernetes API service proxy/no-fallback 路径和显式原始 JSON 按钮;页面不得直接访问 provider-gateway、D601/D518 业务容器端口、NodePort 或 raw v3s/kubectl API。 +- 前端:`用户服务 / K3S Control` React 页面必须只通过 `/api/microservices/k3sctl-adapter/proxy/api/control-plane` 通信,展示控制面状态、manifest、D601/D518 实例、active instance、Kubernetes API service proxy/no-fallback 路径和显式原始 JSON 按钮;页面不得直接访问 provider-gateway、D601/D518 业务容器端口、NodePort 或 raw k3s/kubectl API。 -### Code Queue V3S-Managed +### Code Queue K3S-Managed -当前 Code Queue 作为 `id=code-queue` 的 `v3sctl-managed` 用户服务登记在 `config.json`,业务实例由 D601 v3s 控制面代管,并接入统一 `oa-event-flow` 发布 Trace/STEP 事实事件与读取统计中心: +当前 Code Queue 作为 `id=code-queue` 的 `k3sctl-managed` 用户服务登记在 `config.json`,业务实例由 D601 k3s 控制面代管,并接入统一 `oa-event-flow` 发布 Trace/STEP 事实事件与读取统计中心: -- Orchestrator:`deployment.mode=v3sctl-managed`,`deployment.adapterServiceId=v3sctl-adapter`,`deployment.v3sServiceId=code-queue`,`backend.proxyMode=v3sctl-adapter-http`,`backend.nodeBaseUrl=v3s://code-queue`;backend-core 对 Code Queue 的正式链路只能是 `frontend -> backend-core -> v3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service code-queue:4222`。 -- Direct path ban:`code-queue` 不得再登记 `http://code-queue:4222`、`http://host.docker.internal:4222`、NodePort 或 provider-gateway `microservice.http` 作为业务代理目标;frontend 也不得使用旧 `/api/code-queue-direct` 兼容别名作为 Code Queue 页面数据源。provider-gateway 只允许用于维护 D601/D518、部署 adapter、部署 v3s/k8s 节点或诊断节点本机容器。 +- Orchestrator:`deployment.mode=k3sctl-managed`,`deployment.adapterServiceId=k3sctl-adapter`,`deployment.k3sServiceId=code-queue`,`backend.proxyMode=k3sctl-adapter-http`,`backend.nodeBaseUrl=k3s://code-queue`;backend-core 对 Code Queue 的正式链路只能是 `frontend -> backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service code-queue:4222`。 +- Direct path ban:`code-queue` 不得再登记 `http://code-queue:4222`、`http://host.docker.internal:4222`、NodePort 或 provider-gateway `microservice.http` 作为业务代理目标;frontend 也不得使用旧 `/api/code-queue-direct` 兼容别名作为 Code Queue 页面数据源。provider-gateway 只允许用于维护 D601/D518、部署 adapter、部署 k3s/k8s 节点或诊断节点本机容器。 - 实例语义:D601 是默认 active/single-writer 实例,`CODE_QUEUE_INSTANCE_ID=D601` 且 `CODE_QUEUE_SCHEDULER_ENABLED=true`;D518 是 standby 实例,必须设置 `CODE_QUEUE_INSTANCE_ID=D518`、`CODE_QUEUE_SCHEDULER_ENABLED=false` 和 `CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED=false`,避免两个实例同时消费同一 PostgreSQL 队列或重复回放 OA 统计。D601 active 也默认关闭 `CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED`;历史 OA Trace/STEP 回填必须通过显式 `/api/oa/backfill` 运维动作触发,不能在每次 Pod 重启时自动批量发布旧事件。 -- 部署引用:Code Queue 镜像仍复用 `src/components/microservices/code-queue/Dockerfile`,Kubernetes 运行清单为 `src/components/microservices/v3sctl-adapter/v3s/code-queue.k8s.yaml`,`config.json` 对外记录 v3s manifest `src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json`;主 server 根目录 `docker-compose.yml` 不包含 `code-queue` service,旧 D601 direct Compose 文件只作为迁移/本地诊断参考,不是正式运行入口。 +- 部署引用:Code Queue 镜像仍复用 `src/components/microservices/code-queue/Dockerfile`,Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml`,`config.json` 对外记录 k3s manifest `src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json`;主 server 根目录 `docker-compose.yml` 不包含 `code-queue` service,旧 D601 direct Compose 文件只作为迁移/本地诊断参考,不是正式运行入口。 - 主服务依赖映射:Code Queue 仍以主 PostgreSQL 为权威数据库,但 D601 k3s Pod 不能依赖公网直连 `74.48.78.17:15432/4255`。Pod 内 `DATABASE_URL` 和 `OA_EVENT_FLOW_BASE_URL` 必须指向集群内 `d601-tcp-egress-gateway` Service,再由该 gateway 通过 D601 provider-gateway egress proxy 的 HTTP CONNECT 转发到主 PostgreSQL 和 OA Event Flow;新增 TCP 依赖时扩展 `TCP_EGRESS_ROUTES`,不得在业务容器里新增一次性公网直连或 ad hoc 隧道。D601 active 实例的 `CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL` 直接使用本机 ClaudeQQ 映射 `http://host.docker.internal:3290`。这些端口映射只服务受控节点运行时,必须用防火墙或等价策略限制来源,不得成为浏览器或任意公网客户端入口。 - K8s 探针与启动维护:Kubernetes liveness/startup probe 必须使用轻量 `/live`,readiness 和用户服务健康使用 `/health`;`/health` 不得执行全量任务聚合、历史回填或长事务索引维护,历史任务总览应由 `/api/tasks/overview` 读取 PostgreSQL。启动时允许后台执行队列元数据 flush、通知 outbox 读取、任务表索引维护和 overview warmup,但这些维护不得阻塞 Bun server、readiness endpoint 或 frontend overview;通知表索引和大批量 OA backfill 不得作为默认启动副作用。 -- MiniMax/OpenCode 并发:`minimax-m2.7` 通过 OpenCode JSON 事件端口运行;每个 Code Queue task 必须使用独立的 OpenCode XDG data/config/cache/state 目录,禁止多队列并发任务共享同一个 OpenCode SQLite/WAL 状态目录,否则并发 smoke 会触发 `PRAGMA journal_mode = WAL` 之类的数据库锁或初始化错误。用于验证 v3s/k8s 链路的 MiniMax smoke 以“至少 4 个任务、分布到 2 个 queue、至少 2 个终态成功”为链路验收线;剩余失败如果是 OpenCode 最终回复捕获、业务任务判定或模型限流,应作为 Code Queue 执行可靠性问题单独排查,不能反推 v3s 代理链路失败。 +- MiniMax/OpenCode 并发:`minimax-m2.7` 通过 OpenCode JSON 事件端口运行;每个 Code Queue task 必须使用独立的 OpenCode XDG data/config/cache/state 目录,禁止多队列并发任务共享同一个 OpenCode SQLite/WAL 状态目录,否则并发 smoke 会触发 `PRAGMA journal_mode = WAL` 之类的数据库锁或初始化错误。用于验证 k3s/k8s 链路的 MiniMax smoke 以“至少 4 个任务、分布到 2 个 queue、至少 2 个终态成功”为链路验收线;剩余失败如果是 OpenCode 最终回复捕获、业务任务判定或模型限流,应作为 Code Queue 执行可靠性问题单独排查,不能反推 k3s 代理链路失败。 - 默认出网代理:D601 active Code Queue Pod 必须默认把 `HTTP_PROXY`、`HTTPS_PROXY` 和 `ALL_PROXY` 注入给 Codex/OpenCode、`git`、`curl`、`npm` 等任务子进程;当前唯一上游是 D601 provider-gateway egress HTTP CONNECT 代理,并通过 Kubernetes `Service d601-provider-egress-proxy` 暴露给 `unidesk` namespace 内的 Pod。该 Service 的 EndpointSlice 指向 D601 provider-gateway 私有 Docker network endpoint,Pod 内代理 URL 使用 `http://d601-provider-egress-proxy.unidesk.svc.cluster.local:18789`,provider-gateway 宿主端口仍只允许绑定 `127.0.0.1`,不得开放公网;如 provider-gateway 容器 IP 变化,必须同步刷新 EndpointSlice 并用 Code Queue `/health.egressProxy.connected=true` 验证。这里的 provider-gateway 只承担出网代理,不承担 Code Queue 业务 HTTP 代理;业务访问仍只能走 Kubernetes API service proxy。k3s/k8s 原生 egress gateway、service mesh 或 CNI egress policy 只作为后续网络层增强方向,当前交付态不引入第二套出网控制面。远程开发/执行容器不得只依赖这些环境变量,必须在容器网络层用 TUN 默认路由和 OUTPUT 防火墙强制外网流量只能经 master TUN 出口。 - 出网代理无 fallback 纪律:Code Queue 的运行时配置只允许一个默认出网路径,即 provider-gateway egress proxy;不得在代码中同时保留 Code Queue 自建 WebSocket proxy、临时 shell proxy、D601 本地直连公网、主 server direct HTTP proxy 等隐式分支。任何新增网络 fallback 都必须先进入本参考文档并配套 `/health` 可见状态,否则视为残留旧路径。 -- 上线纪律:Code Queue 相关的前端或后端改进必须在同一任务内正式上线并验证公网 frontend 或 live API,不能只停留在源码、构建产物或“后续再上线”。修改 Code Queue 自身时不得等待当前 Code Queue task 结束、等待 queue idle 或等待 `0 running` 后才重启;D601 active 实例的正式后端部署入口是 `bun scripts/cli.ts codex deploy `,它按已 push 的 remote commit 做 build-first 镜像替换、k3s image import、manifest apply、rollout 和健康验证,并用 v3s adapter、Code Queue live API 或公网 frontend 证明任务和队列仍可读可继续。 +- 上线纪律:Code Queue 相关的前端或后端改进必须在同一任务内正式上线并验证公网 frontend 或 live API,不能只停留在源码、构建产物或“后续再上线”。修改 Code Queue 自身时不得等待当前 Code Queue task 结束、等待 queue idle 或等待 `0 running` 后才重启;D601 active 实例的正式后端部署入口是 `bun scripts/cli.ts codex deploy `,它按已 push 的 remote commit 做 build-first 镜像替换、k3s image import、manifest apply、rollout 和健康验证,并用 k3s adapter、Code Queue live API 或公网 frontend 证明任务和队列仍可读可继续。 - 期望状态部署:新的通用入口是 `bun scripts/cli.ts deploy apply --service code-queue`,它从 `deploy.json` 读取 repo 与 commit,再按 `docs/reference/deploy.md` 的 target-side build 规范在 D601 构建、导入 k3s、rollout 并验证 live commit。`codex deploy ` 是兼容入口,后续实现应复用同一个 reconciler,不得维护第二套部署语义。 -- 更名与灾备恢复:旧版 Codex 队列服务名只允许作为兼容诊断和一次性迁移来源;`code-queue-backend` 容器自身 `/health` 正常但 `microservice health code-queue` 返回 provider 直连错误时,优先判定为 backend-core 仍加载旧 `MICROSERVICES_JSON` 或 adapter manifest 未刷新,必须刷新 `.state/docker-compose.env`、重建/替换 `backend-core` 与 `v3sctl-adapter`,随后用 `microservice list` 验证 `code-queue` 的 `runtime.orchestrator=v3sctl`、`backend.proxyMode=v3sctl-adapter-http` 和无业务容器直连摘要。 +- 更名与灾备恢复:旧版 Codex 队列服务名只允许作为兼容诊断和一次性迁移来源;`code-queue-backend` 容器自身 `/health` 正常但 `microservice health code-queue` 返回 provider 直连错误时,优先判定为 backend-core 仍加载旧 `MICROSERVICES_JSON` 或 adapter manifest 未刷新,必须刷新 `.state/docker-compose.env`、重建/替换 `backend-core` 与 `k3sctl-adapter`,随后用 `microservice list` 验证 `code-queue` 的 `runtime.orchestrator=k3sctl`、`backend.proxyMode=k3sctl-adapter-http` 和无业务容器直连摘要。 - Codex 认证:容器必须从 D601 的 `/home/ubuntu/.codex/config.toml` 同步 Codex provider 配置到 D601 `.state/code-queue/codex-home/config.toml`,并只读挂载 `/home/ubuntu/.codex/auth.json` 到容器 `/root/.codex/auth.json` 后同步到 `.state/code-queue/codex-home/auth.json`,让 `codex app-server` 使用与 host 一致的 provider 登录态;同时通过 D601 `.state/code-queue-d601.env` 或 k8s `code-queue-env` secret 透传 `OPENAI_API_KEY`、`CRS_OAI_KEY` 等 provider 所需变量。这些 provider 环境变量和 auth 文件不得写入仓库,必须由 D601 运行时文件或 k8s secret 注入,确保容器重建和重启后不会丢失认证。新增 provider 的 `env_key` 时必须增加同类运行时透传和 Compose/k8s 持久化,禁止把 Codex 或 MiniMax 密钥写入仓库文件。Code Queue 容器必须只读挂载 D601 WSL host 的 SSH 目录到 `/root/.ssh`(默认 `/home/ubuntu/.ssh`),让容器内 `git push`、`ssh -T git@github.com` 与 WSL host 使用同一套 GitHub SSH key/known_hosts;不得把私钥复制进镜像或仓库。 - Develop-ready 镜像:Code Queue 镜像必须在启动前预装 UniDesk/Pipeline 调试所需工具,至少包含 `codex`、`bun`、`node`、`npm`/`npx`、`git`、`rg`、`curl`、`python3`/`pip3`、`docker`、`docker compose`、`docker-compose`、`jq`、`ssh`、`rsync`、`make`、`gcc`/`g++`、`iptables`、`tar`、`gzip` 和 `unzip`;不得依赖 Codex 任务运行时再 `apt-get install` 这些基础环境。 - 远程开发容器与任务执行 Provider:Code Queue 必须能通过 live API 拉起 D601 等计算节点上的开发容器,入口为 `POST /api/dev-containers//start`,默认 Provider 为 `D601`。该流程由 Code Queue 调用 UniDesk `ssh ` 维护桥在目标节点创建 `unidesk-codex-dev-`,并在 Code Queue 所在节点与开发容器之间建立 `ssh -w` TUN 点对点链路;服务所在节点负责对开发容器的 TUN 源地址做 NAT/MASQUERADE,开发容器默认路由和 DNS 改走该 TUN,从而让 `ping google.com`、DNS、HTTP(S) 等出网都经主 server 全局代理,而不是依赖 D601 本地网络。提交 Code Queue 任务时必须支持选择执行 Provider:`D601` 在 D601 `code-queue-backend` 容器中本机执行,默认工作目录为 `/workspace`,并且 `/workspace` 必须映射 D601 WSL host 的 `/home/ubuntu`,`/root/unidesk` 与 `/app` 必须单独映射 `/home/ubuntu/cq-deploy` 作为服务部署仓库;其他 Provider 在对应 `unidesk-codex-dev-` 容器中执行,默认工作目录为 `/home/ubuntu`,可按任务覆盖 `cwd`。远程任务启动前必须自动复用或拉起该 Provider 的开发容器、同步 Codex 配置和允许的运行时 provider 环境变量,并通过同一 master TUN/NAT 链路出网;目标 host 存在 `/mnt` 时,开发容器必须挂载 host `/mnt:/mnt`,确保 D601 这类 WSL 节点的 Windows 盘符路径如 `/mnt/f/Work/ConStart` 在任务容器内可见,避免 agent 因缺少真实工作区而搜索到无关项目。TUN 建立必须幂等处理 stale 状态:启动前清理旧 `tun`、默认路由、旧 tunnel SSH 进程和旧 OUTPUT 跳转,缺失旧设备不能导致失败,冷启动运行时准备要有有界但足够的 timeout。TUN 建立后必须创建 `UD-CQ-EGRESS-` OUTPUT 链,规则只允许 loopback、既有连接、`tun` 出口以及到 master server 的 SSH tunnel 控制连接,随后 reject 其他 IPv4/IPv6 出站包;这条网络层封口是开发/执行容器的权威外网边界,不能用 `HTTP_PROXY`/`NO_PROXY` 环境变量替代,容器镜像也必须使用已解析出的唯一 `unidesk-code-queue:` 或显式 `image`,缺失时直接失败,禁止 provider-gateway image、`latest` 或其他隐式镜像 fallback。验收必须保留三类日志:容器建隧道后 `ping google.com` 成功、强制指定原 Docker 网卡直连外网被 `sealed_direct_ping=blocked_expected` 拦截、服务所在节点上对应 `UNIDESK-CODEX-DEV-` NAT 链或 `tun` 计数在 ping 前后增长;涉及 WSL 工作区任务时还必须在开发容器内验证目标 `/mnt/...` 路径可读。`GET /api/dev-containers//status` 必须展示默认路由、`route_8_8_8_8`、`egressFirewallChain` 和 OUTPUT 链跳转。开发容器代理密钥只生成到 `.state/code-queue/dev-proxy/` 与目标节点用户目录,不得提交到仓库。 @@ -180,12 +180,12 @@ Baidu Netdisk 在 UniDesk 语境中按纯后端服务管理:不得暴露百度 - 代理路径:只允许 `/health`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`、`PATCH`。Code Queue 只在 Compose 内网暴露 `4222/tcp`,不得映射或开放到公网。 - UniDesk 前端:`用户服务 / Code Queue` React 页面负责展示队列卡片、任务 ID、复制任务 ID、引用按钮、任务耗时、默认模型、模型下拉、执行 Provider 下拉、执行模式下拉、Provider/模式对应默认工作目录、显式入队份数、引用任务 ID、清空输入、创建成功提示、MiniMax judge 状态、Codex CLI-like 输出流、attempt 终态、追加 prompt、打断和手动重试控件;选择 `windows-native` 时应优先切到支持 Windows 原生 Codex 的非主 server Provider,并把工作目录提示切到 `/mnt/` 默认路径;整个 agent loop 消息流统一命名为专有名词 `Trace`,`Trace` 包含 assistant message、user prompt、system event 和 tool call;Code Queue 与 Pipeline/OpenCode messages 必须共用 `src/components/frontend/src/trace.tsx` 的 Trace 公共组件、统一 Trace item 接口和 codex/opencode port 适配层;连续 read/edit/run 工具调用只是在 Trace 内折叠为可展开工具调用组,汇总格式至少包含 `xx read, xx edit, xx run`,并展示读取文件、编辑文件、运行命令和耗时摘要;最近 3 个工具调用保持展开,工具调用内容不得自动换行且必须在工具调用块内部横向滚动,工具调用组展开后不得再增加额外左侧缩进;message 与 prompt 必须自动换行,普通 message 不显示左侧项目符号缩进且永不折叠;点击队列卡片引用按钮必须自动把该任务 ID 写入提交表单的引用任务 ID 输入框;引用任务 ID 创建新任务时必须自动注入 `bun scripts/cli.ts codex task ` 的提示,让 Codex 读取初始 prompt、最后消息和工具摘要后继续;连续执行同一 prompt 应使用 `入队份数` 一次性生成多条队列任务,而不是依赖快速连点按钮;左侧 queue/session 卡片的 `QUEUED` 状态必须显示原因,例如 `QUEUED(PREV TASK)`、`QUEUED(MEM LIMIT)`、`QUEUED(ACTIVE LIMIT)`;原始任务 JSON 只能通过显式 `查看原始JSON` 打开。 -### MDTODO V3S-Managed +### MDTODO K3S-Managed -当前 MDTODO 作为 `id=mdtodo` 的 `v3sctl-managed` 用户服务登记在 `config.json`,用于把 D601 Windows 工作区 `F:\Work\vscode-mdtodo` 从 VS Code 扩展形态拆成 UniDesk 可代理的后端服务: +当前 MDTODO 作为 `id=mdtodo` 的 `k3sctl-managed` 用户服务登记在 `config.json`,用于把 D601 Windows 工作区 `F:\Work\vscode-mdtodo` 从 VS Code 扩展形态拆成 UniDesk 可代理的后端服务: -- Orchestrator:`deployment.mode=v3sctl-managed`,`deployment.adapterServiceId=v3sctl-adapter`,`deployment.v3sServiceId=mdtodo`,`backend.proxyMode=v3sctl-adapter-http`,`backend.nodeBaseUrl=v3s://mdtodo`;正式链路只能是 `frontend -> backend-core -> v3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service mdtodo:4267`。 -- 代码与部署引用:后端源码位于 UniDesk 仓库 `src/components/microservices/mdtodo`,Dockerfile 为 `src/components/microservices/mdtodo/Dockerfile`;v3s manifest 为 `src/components/microservices/v3sctl-adapter/v3s/mdtodo.v3s.json`,Kubernetes 运行清单为 `src/components/microservices/v3sctl-adapter/v3s/mdtodo.k8s.yaml`,镜像名固定为 `unidesk-mdtodo:d601`。 +- Orchestrator:`deployment.mode=k3sctl-managed`,`deployment.adapterServiceId=k3sctl-adapter`,`deployment.k3sServiceId=mdtodo`,`backend.proxyMode=k3sctl-adapter-http`,`backend.nodeBaseUrl=k3s://mdtodo`;正式链路只能是 `frontend -> backend-core -> k3sctl-adapter -> Kubernetes API service proxy -> Kubernetes Service mdtodo:4267`。 +- 代码与部署引用:后端源码位于 UniDesk 仓库 `src/components/microservices/mdtodo`,Dockerfile 为 `src/components/microservices/mdtodo/Dockerfile`;k3s manifest 为 `src/components/microservices/k3sctl-adapter/k3s/mdtodo.k3s.json`,Kubernetes 运行清单为 `src/components/microservices/k3sctl-adapter/k3s/mdtodo.k8s.yaml`,镜像名固定为 `unidesk-mdtodo:d601`。 - 持久化边界:D601 的 `F:\Work\vscode-mdtodo` 先同步到 k3s 可见的 WSL hostPath `/home/ubuntu/cq-deploy/.state/mdtodo-workspace`,Pod 将该目录挂载为 `/workspace`,后端直接读写 Markdown TODO 文件;`.state/mdtodo/logs` 只保存 JSONL 日志,不作为任务权威状态。该服务不得把原 VS Code webview 前端或 VSIX 构建产物作为浏览器入口。 - API:`GET /health`、`GET /live`、`GET /logs`;`GET /api/files`;`GET /api/tasks?file=...`;`GET|PATCH|DELETE /api/tasks/{id}`;`POST /api/tasks`;`GET|PUT /api/content`;`POST /api/execute-command`。`/health` 必须证明 hostPath 可读并至少能扫描到 TODO Markdown 文件。 - 代理路径:只允许 `/health`、`/live`、`/logs` 和 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`PUT`、`PATCH` 和 `DELETE`。业务请求不得退化为 provider-gateway 直连、NodePort 或 D601 本机端口。 @@ -294,8 +294,8 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接 - `bun scripts/cli.ts microservice health claudeqq`、`bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login`、`bun scripts/cli.ts microservice proxy claudeqq /api/events/recent` 和 `bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`:验证 ClaudeQQ 后端、NapCat 容器登录、事件订阅和私有代理链路;消息推送使用 `POST /api/push/text`,不得开放 D601 `3290/3000/3001/6099` 公网端口。 - `bun scripts/cli.ts microservice health todo-note` 与 `bun scripts/cli.ts microservice proxy todo-note /api/instances`:验证主 server Todo Note 后端、PostgreSQL 存储和本机 provider-gateway 私有代理链路。 - `bun scripts/cli.ts microservice health oa-event-flow`、`bun scripts/cli.ts microservice proxy oa-event-flow /api/diagnostics --raw` 与 `bun scripts/cli.ts microservice proxy oa-event-flow '/api/events?tags=service:code-queue&limit=20' --raw`:验证统一 OA 事件流、事件表、tag 查询和统计中心。 -- `bun scripts/cli.ts microservice health v3sctl-adapter` 与 `bun scripts/cli.ts microservice proxy v3sctl-adapter /api/control-plane --raw`:验证 D601 `unidesk-v8s` 控制面 adapter、manifest、D601 active/D518 standby 实例状态、`presentNodeIds=[D601,D518]`、`missingNodeIds=[]` 和 no-fallback 运行路径。 -- `bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`:验证 Code Queue 经过 backend-core -> v3sctl-adapter -> v3s active service 的单一路径;输出不得出现 `serviceId=code-queue` 的 provider-gateway `microservice.http` 业务代理任务,写入、追加 prompt、打断和 readAt/未读状态都必须由 backend 写入 PostgreSQL,frontend 不得用本地存储伪造成功状态。 +- `bun scripts/cli.ts microservice health k3sctl-adapter` 与 `bun scripts/cli.ts microservice proxy k3sctl-adapter /api/control-plane --raw`:验证 D601 `unidesk-k3s` 控制面 adapter、manifest、D601 active/D518 standby 实例状态、`presentNodeIds=[D601,D518]`、`missingNodeIds=[]` 和 no-fallback 运行路径。 +- `bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`:验证 Code Queue 经过 backend-core -> k3sctl-adapter -> k3s active service 的单一路径;输出不得出现 `serviceId=code-queue` 的 provider-gateway `microservice.http` 业务代理任务,写入、追加 prompt、打断和 readAt/未读状态都必须由 backend 写入 PostgreSQL,frontend 不得用本地存储伪造成功状态。 - `bun scripts/cli.ts microservice health filebrowser`、`bun scripts/cli.ts microservice health filebrowser-d601` 与 `bun scripts/cli.ts microservice proxy filebrowser / --max-body-bytes 2000`:验证 D518 主 File Browser 和 D601 备用 File Browser 私有代理链路;浏览器 WebUI 必须通过 `/api/microservices/filebrowser/proxy/` 或 `/api/microservices/filebrowser-d601/proxy/` 访问,不得直接开放 `4251` 公网端口。 - `bun scripts/cli.ts --main-server-ip 74.48.78.17 microservice health findjob`:在计算节点或其他非主 server 主机上通过公网 frontend remote CLI 进行同一验证,不需要主 server SSH key。 @@ -303,7 +303,7 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接 ## Frontend Rules -用户服务前端必须整合到 `src/components/frontend/src/` 下的 TypeScript + React 模块中。`app.tsx` 只做 shell/router 和导入分发,业务页面必须拆成独立 TSX,例如 `todo-note.tsx`、`findjob.tsx`、`pipeline.tsx`、`met-nonlinear.tsx`、`code-queue.tsx`、`v3sctl.tsx`。默认展示必须是业务控件:指标卡、状态徽标、表格、草稿卡片、运行卡片、树形任务、表单控件、结构化材料索引、链接和字段摘要;只有操作员点击 `查看原始JSON` 时才允许打开原始 JSON 弹窗。日志、JSONL 和大块 JSON 不得在主界面按行展示,避免把裸数据伪装成 UI。 +用户服务前端必须整合到 `src/components/frontend/src/` 下的 TypeScript + React 模块中。`app.tsx` 只做 shell/router 和导入分发,业务页面必须拆成独立 TSX,例如 `todo-note.tsx`、`findjob.tsx`、`pipeline.tsx`、`met-nonlinear.tsx`、`code-queue.tsx`、`k3sctl.tsx`。默认展示必须是业务控件:指标卡、状态徽标、表格、草稿卡片、运行卡片、树形任务、表单控件、结构化材料索引、链接和字段摘要;只有操作员点击 `查看原始JSON` 时才允许打开原始 JSON 弹窗。日志、JSONL 和大块 JSON 不得在主界面按行展示,避免把裸数据伪装成 UI。 对于超大业务 JSON,backend-core 可把 `__unideskArrayLimit=:` 作为 frontend-only 代理参数传给 provider-gateway,由 provider-gateway 在返回前裁剪指定 JSON 数组并写入 `_unidesk.arrayLimits` 元数据。该参数只用于控制 UniDesk 展示预览,不能替代业务后端自身分页 API 的长期设计。CLI 的 `microservice proxy` 还会对超过默认阈值的 body 做二次有界预览,防止人工验证时输出爆炸;只有显式 `--raw` 才允许倾倒完整 body。 @@ -315,14 +315,14 @@ ClaudeQQ 在 UniDesk 语境中按消息网关后端服务管理:不得直接 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `pipeline` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:18082` 映射和 `pipeline-v2-control` 容器摘要可见。 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `met-nonlinear` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3288` 映射和 `met-nonlinear-ts` 容器摘要可见。 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `claudeqq` 的 `providerId=D601`、`public=false`、`frontendOnly=true`、仓库 URL、commit id、`127.0.0.1:3290` 映射和 `claudeqq-backend` 容器摘要可见。 -- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `v3sctl-adapter` 为 `providerId=D601`、`deployment.mode=unidesk-direct`、后端私有端口 `127.0.0.1:4266`,并确认 `code-queue` 为 `deployment.mode=v3sctl-managed`、`runtime.orchestrator=v3sctl`、`backend.proxyMode=v3sctl-adapter-http`、`backend.nodeBaseUrl=v3s://code-queue`,且不再显示业务容器直连摘要。 +- 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `k3sctl-adapter` 为 `providerId=D601`、`deployment.mode=unidesk-direct`、后端私有端口 `127.0.0.1:4266`,并确认 `code-queue` 为 `deployment.mode=k3sctl-managed`、`runtime.orchestrator=k3sctl`、`backend.proxyMode=k3sctl-adapter-http`、`backend.nodeBaseUrl=k3s://code-queue`,且不再显示业务容器直连摘要。 - 在主 server 运行 `bun scripts/cli.ts microservice list`,确认 `filebrowser` 和 `filebrowser-d601` 分别显示为 `providerId=D518` 和 `providerId=D601`,均为 `public=false`、`frontendOnly=true`,仓库 URL 为 `https://github.com/filebrowser/filebrowser`,后端映射为 `host.docker.internal:4251`,容器摘要分别为 `unidesk-filebrowser-d518` 和 `unidesk-filebrowser-d601`;列表中不得再出现主 server `filebrowser-main` 容器。 - 运行 `bun scripts/cli.ts microservice health findjob` 与 `bun scripts/cli.ts microservice proxy findjob /api/summary`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 FindJob 后端。 - 运行 `bun scripts/cli.ts microservice health pipeline` 与 `bun scripts/cli.ts microservice proxy pipeline '/api/snapshot?__unideskArrayLimit=registry.components:8,runs:3'`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 Pipeline 后端,且 run/procedure 摘要包含甘特图所需时间字段。 - 运行 `bun scripts/cli.ts microservice health met-nonlinear`、`bun scripts/cli.ts microservice proxy met-nonlinear /api/queue`、`bun scripts/cli.ts microservice proxy met-nonlinear '/api/projects?root=projects&limit=20'` 和 `bun scripts/cli.ts microservice proxy met-nonlinear /api/images`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 MET Nonlinear TS 后端。 - 运行 `bun scripts/cli.ts microservice health claudeqq`、`bun scripts/cli.ts microservice proxy claudeqq /api/napcat/login`、`bun scripts/cli.ts microservice proxy claudeqq /api/events/recent` 和 `bun scripts/cli.ts microservice proxy claudeqq /api/events/subscriptions`,确认真实链路经过 backend-core、WebSocket、D601 provider-gateway 和 D601 本机 ClaudeQQ 后端;在 D601 上 `curl http://127.0.0.1:3290/health` 应显示 `service=claudeqq`、`pureBackend=true`、`napcat.containerized=true`、NapCat HTTP/WS 状态、二维码状态和订阅计数。 - 运行 `bun scripts/cli.ts microservice health todo-note` 与 `bun scripts/cli.ts microservice proxy todo-note /api/instances`,确认真实链路经过 backend-core、WebSocket、main-server provider-gateway 和主 server `todo-note-backend` 后端;输出中必须包含五个迁移清单和 PostgreSQL 存储健康状态。 -- 运行 `bun scripts/cli.ts microservice health v3sctl-adapter`、`bun scripts/cli.ts microservice proxy v3sctl-adapter /api/control-plane --raw`、`bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`,确认真实链路经过 backend-core -> v3sctl-adapter -> v3s active service;Code Queue `/health` 必须仍返回业务后端自己的 `queue.storage.primary=postgres`、`queue.storage.postgresReady=true`、`queue.notifications.claudeqq.outbox.storage=postgres` 和 `egressProxy.connected=true`,不得被 adapter 聚合健康 JSON 替代。还必须在 active Code Queue Pod 内验证主 PostgreSQL 端口映射、主 OA Event Flow 端口映射、本机 ClaudeQQ `http://host.docker.internal:3290` 和 `d601-provider-egress-proxy` 均可访问,并在 adapter 控制页确认 D601 active serving healthy、D518 standby pod ready、`missingNodeIds=[]` 且整体不退化为 hidden fallback。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认队列串行推进、输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Code Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 active 实例后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知。Code Queue 服务名、表名前缀或持久化目录发生迁移后,还必须运行 `bun scripts/cli.ts e2e run --only microservice:catalog-code-queue,microservice:code-queue-status,microservice:code-queue-health,microservice:code-queue-tasks`,证明 backend-core catalog、v3s adapter 私有代理、PostgreSQL 队列和任务列表都指向 `code-queue`。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。 +- 运行 `bun scripts/cli.ts microservice health k3sctl-adapter`、`bun scripts/cli.ts microservice proxy k3sctl-adapter /api/control-plane --raw`、`bun scripts/cli.ts microservice health code-queue` 与 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview`,确认真实链路经过 backend-core -> k3sctl-adapter -> k3s active service;Code Queue `/health` 必须仍返回业务后端自己的 `queue.storage.primary=postgres`、`queue.storage.postgresReady=true`、`queue.notifications.claudeqq.outbox.storage=postgres` 和 `egressProxy.connected=true`,不得被 adapter 聚合健康 JSON 替代。还必须在 active Code Queue Pod 内验证主 PostgreSQL 端口映射、主 OA Event Flow 端口映射、本机 ClaudeQQ `http://host.docker.internal:3290` 和 `d601-provider-egress-proxy` 均可访问,并在 adapter 控制页确认 D601 active serving healthy、D518 standby pod ready、`missingNodeIds=[]` 且整体不退化为 hidden fallback。再通过公网 frontend 提交一个 `gpt-5.5` 小任务,确认队列串行推进、输出实时更新、结束后有 judge 判定,且运行中可追加 prompt 或打断。Code Queue 的重启恢复必须作为验收项:运行中任务存在时重启或重建 active 实例后,任务必须从 PostgreSQL 恢复到可继续执行状态,不能丢失 active task、`promptHistory`、后续 queued 任务、readAt/未读状态或已入 outbox 的 ClaudeQQ 通知。Code Queue 服务名、表名前缀或持久化目录发生迁移后,还必须运行 `bun scripts/cli.ts e2e run --only microservice:catalog-code-queue,microservice:code-queue-status,microservice:code-queue-health,microservice:code-queue-tasks`,证明 backend-core catalog、k3s adapter 私有代理、PostgreSQL 队列和任务列表都指向 `code-queue`。批量验收必须通过公网 frontend 设置 `入队份数=5` 或使用多段 prompt 分隔,一次性入队 5 条任务,并确认 5 条任务按顺序进入 running/judging/succeeded,而不是只运行第一条。 - Code Queue 内存防回归验收:凡是改动 Code Queue 的持久化、scheduler、输出/Trace、health、列表/详情查询、日志导出或容器运行参数,交付前必须在 D601 用 `kubectl -n unidesk get deploy,pod,svc,endpoints -o wide`、`kubectl -n unidesk describe deploy/code-queue` 或等价 Docker inspect 确认 memory/swap 硬上限符合预算,运行 `kubectl -n unidesk top pod` 或 Docker stats 确认常驻内存、`OOMKilled=false` 和 `RestartCount` 未异常增长,再运行 `bun scripts/cli.ts microservice health code-queue` 确认 `/health` 是轻量 readiness 且暴露 PostgreSQL/notification/outbox 状态。验收还必须覆盖有历史任务存在时的 `/api/tasks/overview`、单任务详情和 output/transcript 查询,证明热状态裁剪不会丢历史输出、也不会重新把全部历史 `task_json` 缓存在进程内;涉及 TypeScript/frontend 验证的任务应能在 D601 Code Queue memory/swap 预算中完成 `bun run --cwd src/components/frontend check` 这类短时高内存命令,而不是被 memory watchdog 反复 SIGTERM。 - Code Queue 延迟防回归验收:凡是改动 Code Queue 列表、overview、readAt、Trace/summary 懒加载、实时 output/SSE 事件发布、frontend 请求策略、backend-core 用户服务代理或 frontend Code Queue 请求路径,交付前必须在有历史任务数据且有 active output 流动的 live 环境验证 `GET /api/tasks/overview`、`POST /api/tasks//read`、选定 task 的 `trace-step` 和前端 `/app/code-queue/` 首屏均低于 1s 目标;可运行 `bun scripts/src/code-queue-perf.ts --json --target-ms 1000` 采集公网 frontend 下的首屏耗时、最慢 API 和 DOM 完成指标,并用 `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview --raw`、D601 Pod `/health` 与 `/api/tasks/overview` curl、性能面板 `/api/performance` 与 `/api/frontend-performance` 失败/慢操作记录、`kubectl -n unidesk top pod` 或 Docker stats 补充后端耗时、代理 502 和内存/CPU 证据。验收结论必须同时说明是否使用了短 TTL cache、cache 如何被 mutation 或 archive append 失效、数据库索引/聚合是否命中、输出热路径是否只读增量指标,以及分页加载是否跳过 selected/active/stats;不能只展示 cache 命中后的单次快照。 - 运行 `bun scripts/cli.ts microservice health filebrowser`、`bun scripts/cli.ts microservice health filebrowser-d601` 和 `bun scripts/cli.ts microservice proxy filebrowser / --max-body-bytes 2000`,确认 File Browser health 返回 `status=OK`,WebUI HTML 包含 `File Browser`,D518/D601 通过 provider-gateway 访问节点本机 `4251`;随后在公网 frontend 的 `用户服务 / File Browser` 中确认 D518 为默认目标、可导出截图、iframe 紧凑布局不再有巨大 `folder` 标记遮挡文件名,并可浏览 `/mnt/c`。 diff --git a/docs/reference/observability.md b/docs/reference/observability.md index 1bb66c13..6f3dab7a 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -32,6 +32,6 @@ frontend Bun server 必须提供同源 `/api/frontend-performance`,记录 webu 性能优化必须先用这些指标锁定慢操作名称、路径、耗时和代理层级,再改后端查询或前后端通信策略;不得只凭主观体感改 UI。Code Queue 这类控制面页面出现 `core_proxy`、`GET /api/microservices/code-queue/proxy/api/tasks/overview`、`POST /api/microservices/code-queue/proxy/api/tasks//read` 等超过 1s 的慢操作时,应保留优化前后的性能面板证据,并同时记录 live API 耗时、容器内存、`/health` 存储摘要和是否仍通过 PostgreSQL/append-only archive 重建历史数据。短 TTL cache、warmup 或页面内存缓存只能作为重复请求抖动保护,性能证据必须证明数据库索引/聚合、分页和渐进式披露本身已把核心路径降到目标内,不能用长缓存遮蔽慢 SQL 或全量 JSON 物化。 -当最近失败请求集中出现 frontend `core_proxy` 502,路径为 `/api/microservices/code-queue/proxy/...` 的 overview、trace 或 summary,且 v3s/k8s Pod 仍在运行时,必须区分“Kubernetes API service proxy 不可达”“Code Queue 进程不可达”和“Code Queue event loop 被热路径同步工作饿死”。排障顺序是同时查看 `/api/frontend-performance`、`/api/performance`、`v3sctl-adapter` `/api/control-plane`、Kubernetes Pod `/live`、`/health`、overview/trace-step curl、`kubectl top pod` 或 Docker stats、容器 `RestartCount`/`OOMKilled` 和 Code Queue 日志;如果 Pod 内 `/health` 也超时,应优先检查实时 output 发布、archive 读取、transcript 构建、统计计算、启动维护、历史 OA backfill 和远程 Provider 准备/SSH 子进程是否阻塞 event loop,而不是先调整 frontend 渲染或代理超时。Code Queue 默认不得在启动时自动执行历史 OA backfill 或通知表索引维护;显式 backfill 必须作为运维动作记录,并在运行期间并发证明 `/live`、`/health` 与 `/api/tasks/overview` 仍快速返回。涉及 D601 等远程 Provider 时,还要检查 `runCodeQueueSsh`/开发容器准备是否仍存在同步子进程、无 timeout 的 SSH、无上限 stdout/stderr 或 stale TUN 重建等待;修复后必须在远程准备探针运行期间并发证明 Pod `/health` 与 `/api/tasks/overview` 仍快速返回。 +当最近失败请求集中出现 frontend `core_proxy` 502,路径为 `/api/microservices/code-queue/proxy/...` 的 overview、trace 或 summary,且 k3s/k8s Pod 仍在运行时,必须区分“Kubernetes API service proxy 不可达”“Code Queue 进程不可达”和“Code Queue event loop 被热路径同步工作饿死”。排障顺序是同时查看 `/api/frontend-performance`、`/api/performance`、`k3sctl-adapter` `/api/control-plane`、Kubernetes Pod `/live`、`/health`、overview/trace-step curl、`kubectl top pod` 或 Docker stats、容器 `RestartCount`/`OOMKilled` 和 Code Queue 日志;如果 Pod 内 `/health` 也超时,应优先检查实时 output 发布、archive 读取、transcript 构建、统计计算、启动维护、历史 OA backfill 和远程 Provider 准备/SSH 子进程是否阻塞 event loop,而不是先调整 frontend 渲染或代理超时。Code Queue 默认不得在启动时自动执行历史 OA backfill 或通知表索引维护;显式 backfill 必须作为运维动作记录,并在运行期间并发证明 `/live`、`/health` 与 `/api/tasks/overview` 仍快速返回。涉及 D601 等远程 Provider 时,还要检查 `runCodeQueueSsh`/开发容器准备是否仍存在同步子进程、无 timeout 的 SSH、无上限 stdout/stderr 或 stale TUN 重建等待;修复后必须在远程准备探针运行期间并发证明 Pod `/health` 与 `/api/tasks/overview` 仍快速返回。 Code Queue task 明明产出最终回复却反复 `retry_wait` 时,应优先用任务详情里的 latest attempt 字段核查 `terminalStatus`、`transportClosedBeforeTerminal`、`appServerExitCode`、`finalResponseChars`、`judge.raw._safetyOverride` 和 attempt output。OpenCode 远程任务中,`opencode completed status=completed exit=0` 加当前 attempt 非空 assistant 输出应对应 `terminalStatus=completed`、`transportClosedBeforeTerminal=false`;如果因为缺少 `step_finish` 事件仍触发 `_safetyOverride=terminal_not_completed`,说明协议终态归一化有回归。相反,当前 attempt 没有最终 assistant response 时即使 tool/read/bash 证据完整,也必须 retry,不能用旧 `task.finalResponse` 或 reasoning/tool evidence 代替可见最终回复。 diff --git a/docs/reference/provider-gateway.md b/docs/reference/provider-gateway.md index 34ccb9f0..c0297d9f 100644 --- a/docs/reference/provider-gateway.md +++ b/docs/reference/provider-gateway.md @@ -86,17 +86,17 @@ provider ingress 是唯一允许公网暴露的 provider 连接接口,当前 ## User Service HTTP Proxy -`microservice.http` 是 provider-gateway 给 `deployment.mode=unidesk-direct` 用户服务使用的私有后端访问能力。backend-core 通过真实 WebSocket dispatch 下发目标 service id、节点本机 `targetBaseUrl`、path、query、method、request body、timeout 和可选 JSON 数组裁剪参数;provider-gateway 支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`,但最终允许方法必须由每个用户服务的 `backend.allowedMethods` 显式配置。provider-gateway 只允许访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这些节点本地地址;主 server 内置 Todo Note 后端可使用 Compose 服务名 `http://todo-note:4211`。`deployment.mode=v3sctl-managed` 的 Code Queue 不得通过 provider-gateway `microservice.http` 直连业务容器,正式路径只能是 backend-core -> `v3sctl-adapter` -> Kubernetes API service proxy -> v3s/k8s Service。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。 +`microservice.http` 是 provider-gateway 给 `deployment.mode=unidesk-direct` 用户服务使用的私有后端访问能力。backend-core 通过真实 WebSocket dispatch 下发目标 service id、节点本机 `targetBaseUrl`、path、query、method、request body、timeout 和可选 JSON 数组裁剪参数;provider-gateway 支持 `GET`、`HEAD`、`POST`、`PUT`、`PATCH`、`DELETE`,但最终允许方法必须由每个用户服务的 `backend.allowedMethods` 显式配置。provider-gateway 只允许访问 `http://127.0.0.1`、`http://localhost`、`http://host.docker.internal` 这些节点本地地址;主 server 内置 Todo Note 后端可使用 Compose 服务名 `http://todo-note:4211`。`deployment.mode=k3sctl-managed` 的 Code Queue 不得通过 provider-gateway `microservice.http` 直连业务容器,正式路径只能是 backend-core -> `k3sctl-adapter` -> Kubernetes API service proxy -> k3s/k8s Service。该能力不打开 provider-gateway 入站端口,也不替代业务仓库自身 Dockerfile/docker-compose。 超大 JSON 响应可以使用 `jsonArrayLimits` 在 provider-gateway 返回前裁剪指定数组,并在响应体中写入 `_unidesk.arrayLimits` 元数据,便于 UniDesk frontend 预览列表而不展示裸 JSON。长期应优先推动业务后端提供分页 API;裁剪只是 UniDesk 集成层的展示保护。 ## Egress Proxy -provider-gateway 可以提供 egress HTTP CONNECT 代理,用于让 Code Queue、Pipeline runner、target-side Docker build 等节点侧执行环境通过既有 provider WebSocket 通道出网。代理默认监听容器内 `0.0.0.0:18789`,节点部署必须只发布为宿主 loopback `127.0.0.1:18789->18789/tcp`,不得开放公网端口;普通 Docker 执行容器可通过同一私有 Docker network 访问 provider-gateway 容器名,v3s/k8s Pod 必须通过显式 Kubernetes Service/EndpointSlice 暴露同节点 provider-gateway 私有 endpoint,例如 D601 Code Queue 使用 `d601-provider-egress-proxy.unidesk.svc.cluster.local:18789`,不得把该 egress Service 当作业务 HTTP 入口。代理只负责把本地 CONNECT/absolute HTTP 请求转换为 `egress_tcp_open`、`egress_tcp_data`、`egress_tcp_close` 消息;backend-core 在主 server 侧建立真实 TCP 连接并把数据回传,避免 D601 等计算节点本地网络不可达时卡死 Codex/Git/NPM/apt/Playwright。 +provider-gateway 可以提供 egress HTTP CONNECT 代理,用于让 Code Queue、Pipeline runner、target-side Docker build 等节点侧执行环境通过既有 provider WebSocket 通道出网。代理默认监听容器内 `0.0.0.0:18789`,节点部署必须只发布为宿主 loopback `127.0.0.1:18789->18789/tcp`,不得开放公网端口;普通 Docker 执行容器可通过同一私有 Docker network 访问 provider-gateway 容器名,k3s/k8s Pod 必须通过显式 Kubernetes Service/EndpointSlice 暴露同节点 provider-gateway 私有 endpoint,例如 D601 Code Queue 使用 `d601-provider-egress-proxy.unidesk.svc.cluster.local:18789`,不得把该 egress Service 当作业务 HTTP 入口。代理只负责把本地 CONNECT/absolute HTTP 请求转换为 `egress_tcp_open`、`egress_tcp_data`、`egress_tcp_close` 消息;backend-core 在主 server 侧建立真实 TCP 连接并把数据回传,避免 D601 等计算节点本地网络不可达时卡死 Codex/Git/NPM/apt/Playwright。 该能力属于 provider-gateway 通道能力,register/heartbeat 的 `unideskCapabilities` 必须包含 `network.egress-proxy`,labels 必须上报 `providerGatewayEgressProxy*` 状态。不得再为某个用户服务单独注册伪 provider 来实现出网代理;否则节点列表会出现虚假 provider,且代理、统计、升级路径会形成多套通道。代理健康检查使用 `GET /__unidesk/egress-proxy/health`,返回 `connected`、`providerId`、`activeTunnels` 和监听端口;业务服务自己的 `/health` 应把该结果作为排障证据透出。 -egress proxy 的长期边界是“统一 provider 通道,不引入第二控制面”。backend-core 只接受在线 provider socket 上的 `egress_tcp_*` 消息,并在该 socket 关闭时销毁全部对应 TCP relay;provider-gateway 只维护本地 HTTP proxy 与 WebSocket 消息映射,不保存业务状态,不参与任务调度、统计或节点注册以外的控制面。执行容器、用户服务、Pipeline runner 和 provider-side deploy build 不允许直接连接 backend-core provider ingress,也不允许携带 provider token 自行注册;需要出网时只能连接同节点 provider-gateway 的私有 proxy endpoint。当前 v3s/k8s Code Queue 通过 `d601-provider-egress-proxy` Kubernetes Service 连接 D601 provider-gateway egress endpoint,这是 Pod 内的出网入口,不是业务 HTTP 代理入口,也不能替代 Kubernetes API service proxy。部署构建同样不得新建 SSH SOCKS、公网 master proxy 或宿主全局代理;构建脚本只能把 provider-gateway WS egress 作为短生命周期环境变量和 Docker build-arg 注入,并配合目标节点本地 BuildKit/image cache 避免重复下载大依赖层。 +egress proxy 的长期边界是“统一 provider 通道,不引入第二控制面”。backend-core 只接受在线 provider socket 上的 `egress_tcp_*` 消息,并在该 socket 关闭时销毁全部对应 TCP relay;provider-gateway 只维护本地 HTTP proxy 与 WebSocket 消息映射,不保存业务状态,不参与任务调度、统计或节点注册以外的控制面。执行容器、用户服务、Pipeline runner 和 provider-side deploy build 不允许直接连接 backend-core provider ingress,也不允许携带 provider token 自行注册;需要出网时只能连接同节点 provider-gateway 的私有 proxy endpoint。当前 k3s/k8s Code Queue 通过 `d601-provider-egress-proxy` Kubernetes Service 连接 D601 provider-gateway egress endpoint,这是 Pod 内的出网入口,不是业务 HTTP 代理入口,也不能替代 Kubernetes API service proxy。部署构建同样不得新建 SSH SOCKS、公网 master proxy 或宿主全局代理;构建脚本只能把 provider-gateway WS egress 作为短生命周期环境变量和 Docker build-arg 注入,并配合目标节点本地 BuildKit/image cache 避免重复下载大依赖层。 故障语义必须显式,不允许静默 fallback。provider-gateway 到 backend-core 的 WebSocket 未连接时,本地 proxy 必须返回 503;执行容器不能自动绕过到 D601 本地直连公网、外部公共代理或主 server 公网 HTTP 端口。`NO_PROXY` 只用于 PostgreSQL、OA Event Flow、ClaudeQQ、frontend/backend-core 内网代理、provider-gateway health 等明确内网链路,不能把 GitHub、模型 API、npm registry 等外部目标加入绕过列表。`hyueapi.com` 是明确的模型 API 例外:该上游会拒绝 provider-gateway egress proxy 出口,Code Queue 必须用 `CODE_QUEUE_EGRESS_PROXY_NO_PROXY` / `NO_PROXY` 将 `hyueapi.com,.hyueapi.com` 配成直连,其它模型 API 仍不得默认绕过 proxy。验收必须同时证明 provider-gateway labels、业务服务 `/health` 和执行容器内 `curl -I https://...` 都走同一 proxy path,hyueapi 例外则以 Code Queue `/health.egressProxy.noProxy` 和目标任务成功完成作为证据。 diff --git a/docs/reference/repo-tree.md b/docs/reference/repo-tree.md index b1a0f836..7e394e2c 100644 --- a/docs/reference/repo-tree.md +++ b/docs/reference/repo-tree.md @@ -73,7 +73,7 @@ - src/met-nonlinear.tsx (MET Nonlinear D601 training orchestration React page; do not fold back into `app.tsx`) - src/code-queue.tsx (Code Queue user-service React page; do not fold back into `app.tsx`) - src/oa-event-flow.tsx (Unified OA event flow and Trace/STEP stats React page; do not fold back into `app.tsx`) - - src/v3sctl.tsx (V3S Control Plane React page backed only by `v3sctl-adapter`; do not fold back into `app.tsx`) + - src/k3sctl.tsx (K3S Control Plane React page backed only by `k3sctl-adapter`; do not fold back into `app.tsx`) - public/ (HTML/CSS static assets for the compact industrial console; no handwritten app JS) - provider-gateway/ (Compute node Provider Gateway container) - package.json @@ -85,7 +85,7 @@ - config/postgresql.conf - init/001_unidesk_init.sql - microservices/ (UniDesk-owned user services and compatibility examples) - - code-queue/ (Codex/OpenCode queue backend; v3s-managed when exposed through UniDesk) + - code-queue/ (Codex/OpenCode queue backend; k3s-managed when exposed through UniDesk) - oa-event-flow/ (Unified OA event ledger, tag stream, and Trace/STEP stats center) - - v3sctl-adapter/ (D601 v3s control-plane adapter and managed service manifests) + - k3sctl-adapter/ (D601 k3s control-plane adapter and managed service manifests) - example-service/ diff --git a/scripts/src/check.ts b/scripts/src/check.ts index 1c233a3d..e25ee7d4 100644 --- a/scripts/src/check.ts +++ b/scripts/src/check.ts @@ -34,7 +34,7 @@ function unifiedLogRotationItem(): CheckItem { "src/components/frontend/src/index.ts", "src/components/provider-gateway/src/index.ts", "src/components/microservices/code-queue/src/index.ts", - "src/components/microservices/v3sctl-adapter/src/index.ts", + "src/components/microservices/k3sctl-adapter/src/index.ts", "src/components/microservices/mdtodo/src/index.ts", "src/components/microservices/project-manager/src/index.ts", "src/components/microservices/baidu-netdisk/src/index.ts", @@ -68,7 +68,7 @@ export function runChecks(config: UniDeskConfig): { ok: boolean; items: CheckIte fileItem("src/components/frontend/src/index.ts"), fileItem("src/components/provider-gateway/src/index.ts"), fileItem("src/components/microservices/oa-event-flow/src/index.ts"), - fileItem("src/components/microservices/v3sctl-adapter/src/index.ts"), + fileItem("src/components/microservices/k3sctl-adapter/src/index.ts"), fileItem("src/components/microservices/mdtodo/src/index.ts"), fileItem("scripts/src/deploy.ts"), fileItem("scripts/src/e2e.ts"), diff --git a/scripts/src/config.ts b/scripts/src/config.ts index a733e8f4..519d932c 100644 --- a/scripts/src/config.ts +++ b/scripts/src/config.ts @@ -67,9 +67,9 @@ export interface UniDeskMicroserviceConfig { timeoutMs: number; }; deployment: { - mode: "unidesk-direct" | "v3sctl-managed"; + mode: "unidesk-direct" | "k3sctl-managed"; adapterServiceId?: string; - v3sServiceId?: string; + k3sServiceId?: string; namespace?: string; expectedNodeIds?: string[]; activeNodeId?: string; @@ -169,8 +169,8 @@ function microserviceConfig(item: Record, index: number): UniDe const frontend = asRecord(item.frontend, `${path}.frontend`); const deployment = item.deployment === undefined ? undefined : asRecord(item.deployment, `${path}.deployment`); const deploymentMode = deployment === undefined ? "unidesk-direct" : stringField(deployment, "mode", `${path}.deployment`); - if (deploymentMode !== "unidesk-direct" && deploymentMode !== "v3sctl-managed") { - throw new Error(`${path}.deployment.mode must be unidesk-direct or v3sctl-managed`); + if (deploymentMode !== "unidesk-direct" && deploymentMode !== "k3sctl-managed") { + throw new Error(`${path}.deployment.mode must be unidesk-direct or k3sctl-managed`); } return { id: stringField(item, "id", path), @@ -202,7 +202,7 @@ function microserviceConfig(item: Record, index: number): UniDe : { mode: deploymentMode, adapterServiceId: optionalStringField(deployment, "adapterServiceId", `${path}.deployment`), - v3sServiceId: optionalStringField(deployment, "v3sServiceId", `${path}.deployment`), + k3sServiceId: optionalStringField(deployment, "k3sServiceId", `${path}.deployment`), namespace: optionalStringField(deployment, "namespace", `${path}.deployment`), expectedNodeIds: optionalStringArrayField(deployment, "expectedNodeIds", `${path}.deployment`), activeNodeId: optionalStringField(deployment, "activeNodeId", `${path}.deployment`), diff --git a/scripts/src/deploy.ts b/scripts/src/deploy.ts index 7fb0ac23..00d95bad 100644 --- a/scripts/src/deploy.ts +++ b/scripts/src/deploy.ts @@ -79,10 +79,10 @@ const shortRemoteTimeoutMs = 20_000; const pollIntervalMs = 5_000; const remoteDeployRoot = "/home/ubuntu/.unidesk/deploy"; const k8sNamespace = "unidesk"; -const k8sContainer = "unidesk-v8s-server"; -const k8sKubeconfig = "/home/ubuntu/cq-deploy/.state/v8s/kubeconfig"; -const v3sDeployDir = "/home/ubuntu/cq-deploy"; +const k8sKubeconfig = "/etc/rancher/k3s/k3s.yaml"; +const k3sDeployDir = "/home/ubuntu/cq-deploy"; const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789"; +const nativeK3sInstallVersion = "v1.34.1+k3s1"; function nowIso(): string { return new Date().toISOString(); @@ -232,7 +232,7 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str } function targetWorkDir(service: UniDeskMicroserviceConfig): string { - if (service.deployment.mode === "v3sctl-managed") return v3sDeployDir; + if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir; if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") { return rootPath(".state", "deploy", "work", safeId(service.id)); } @@ -240,7 +240,7 @@ function targetWorkDir(service: UniDeskMicroserviceConfig): string { } function buildImageTag(service: UniDeskMicroserviceConfig): string { - if (service.deployment.mode === "v3sctl-managed") return `unidesk-${service.id}:d601`; + if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:d601`; if (targetIsMain(service)) { if (["project-manager", "baidu-netdisk", "oa-event-flow"].includes(service.repository.composeService)) return service.repository.composeService; return `unidesk-${service.repository.composeService}`; @@ -270,8 +270,8 @@ function directDockerfileOverride(service: UniDeskMicroserviceConfig): string { function k8sManifestPath(service: UniDeskMicroserviceConfig): string { const composeFile = service.repository.composeFile; - if (!composeFile.endsWith(".v3s.json")) throw new Error(`${service.id} v3s service composeFile must point to *.v3s.json`); - return composeFile.replace(/\.v3s\.json$/u, ".k8s.yaml"); + if (!composeFile.endsWith(".k3s.json")) throw new Error(`${service.id} k3s service composeFile must point to *.k3s.json`); + return composeFile.replace(/\.k3s\.json$/u, ".k8s.yaml"); } function sourceProxyPrelude(service: UniDeskMicroserviceConfig): string { @@ -510,16 +510,67 @@ function writeComposeEnvFallbackPath(): string { return rootPath(".state", "docker-compose.env"); } +function rootAccessPrelude(): string[] { + return [ + "root_exec() {", + " if [ \"$(id -u)\" = \"0\" ]; then \"$@\"; return; fi", + " if sudo -n true >/dev/null 2>&1; then sudo -n \"$@\"; return; fi", + " if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- \"$@\"; return; fi", + " echo 'native_k3s_root_access=missing' >&2", + " return 1", + "}", + "root_shell() {", + " script=\"$1\"", + " if [ \"$(id -u)\" = \"0\" ]; then bash -lc \"$script\"; return; fi", + " if sudo -n true >/dev/null 2>&1; then sudo -n bash -lc \"$script\"; return; fi", + " if [ -x /mnt/c/Windows/System32/wsl.exe ]; then /mnt/c/Windows/System32/wsl.exe -u root -- bash -lc \"$script\"; return; fi", + " echo 'native_k3s_root_access=missing' >&2", + " return 1", + "}", + ]; +} + +function ensureNativeK3sScript(): string { + const installCommand = [ + `INSTALL_K3S_VERSION=${nativeK3sInstallVersion}`, + "INSTALL_K3S_EXEC=\"server --disable traefik --disable servicelb --disable metrics-server --node-name D601 --node-label unidesk.ai/node-id=D601 --node-label unidesk.ai/provider-id=D601 --tls-san 127.0.0.1 --tls-san host.docker.internal --write-kubeconfig-mode 644\"", + "sh /tmp/unidesk-install-k3s.sh", + ].join(" "); + return [ + "set -euo pipefail", + ...rootAccessPrelude(), + "if ! command -v k3s >/dev/null 2>&1; then", + " curl -fsSL --max-time 60 https://get.k3s.io -o /tmp/unidesk-install-k3s.sh", + ` root_shell ${shellQuote(installCommand)}`, + "fi", + "if ! systemctl is-active --quiet k3s; then", + " root_exec systemctl enable --now k3s", + "fi", + "for attempt in $(seq 1 60); do", + ` if KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes >/dev/null 2>&1; then break; fi`, + " sleep 2", + "done", + `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes -l unidesk.ai/node-id=D601 --no-headers | grep -q .`, + "legacy_k3s_containers=$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /^rancher\\/k3s:/ {print $1}')", + "for container in $legacy_k3s_containers; do", + " docker update --restart=no \"$container\" >/dev/null 2>&1 || true", + " docker stop \"$container\" >/dev/null", + " echo stopped_containerized_k3s=$container", + "done", + `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl get nodes -o wide`, + "printf 'native_k3s=ready kubeconfig=%s\\n' /etc/rancher/k3s/k3s.yaml", + ].join("\n"); +} + function importK3sImageScript(service: UniDeskMicroserviceConfig): string { const image = buildImageTag(service); return [ "set -euo pipefail", + ...rootAccessPrelude(), `image=${shellQuote(image)}`, - `k3s_container=${shellQuote(k8sContainer)}`, "docker image inspect \"$image\" >/dev/null", - "docker ps --format '{{.Names}}' | grep -Fx \"$k3s_container\" >/dev/null", - "docker save \"$image\" | docker exec -i \"$k3s_container\" ctr -n k8s.io images import -", - "docker exec \"$k3s_container\" ctr -n k8s.io images ls | grep -F \"$image\" || true", + "docker save \"$image\" | root_exec k3s ctr -n k8s.io images import -", + "root_exec k3s ctr -n k8s.io images ls | grep -F \"$image\" || true", ].join("\n"); } @@ -629,7 +680,7 @@ function runtimeCommitVerified( desired: string, ): boolean { if (healthCommit !== null && healthCommit.length > 0 && !commitMatches(healthCommit, desired)) return false; - if (service.deployment.mode === "v3sctl-managed") return commitMatches(orchestratorCommit, desired); + if (service.deployment.mode === "k3sctl-managed") return commitMatches(orchestratorCommit, desired); return commitMatches(imageCommit, desired); } @@ -820,7 +871,7 @@ async function readDockerImageCommit(config: UniDeskConfig, service: UniDeskMicr } async function readK8sCommit(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise { - if (service.deployment.mode !== "v3sctl-managed") return null; + if (service.deployment.mode !== "k3sctl-managed") return null; const result = await runTargetCommand(config, service, k8sCommitProbeScript(service), "/home/ubuntu", 30_000, 20_000); const commit = parseFullCommit(result.stdout); return commit.length > 0 ? commit : null; @@ -961,7 +1012,9 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi const build = await step(config, service, "docker-build", buildScript, targetWorkDir(service), Math.min(options.timeoutMs, 540_000), !targetIsMain(service)); if (!pushStep(steps, build)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; - if (service.deployment.mode === "v3sctl-managed") { + if (service.deployment.mode === "k3sctl-managed") { + const nativeK3s = await step(config, service, "ensure-native-k3s", ensureNativeK3sScript(), "/home/ubuntu", 180_000, true); + if (!pushStep(steps, nativeK3s)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const imageImport = await step(config, service, "import-k3s-image", importK3sImageScript(service), targetWorkDir(service), 180_000, true); if (!pushStep(steps, imageImport)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const apply = await step(config, service, "kubectl-apply", applyK8sScript(service), targetWorkDir(service), 60_000, false); diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index 5eefe0e9..2ba99e05 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -61,8 +61,8 @@ const SERVICE_CHECK_NAMES = [ "microservice:catalog-todo-note", "microservice:catalog-oa-event-flow", "microservice:catalog-code-queue", - "microservice:v3sctl-adapter-status", - "microservice:v3sctl-control-plane", + "microservice:k3sctl-adapter-status", + "microservice:k3sctl-control-plane", "microservice:catalog-filebrowser", "microservice:filebrowser-health", "microservice:filebrowser-webui", @@ -1028,8 +1028,8 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 const oaEventFlowEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?limit=10"); const oaEventFlowPipelineEvents = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/events?tags=service:pipeline&limit=10"); const oaEventFlowStats = dockerCoreJson("/api/microservices/oa-event-flow/proxy/api/stats/trace?limit=10"); - const v3sctlStatus = dockerCoreJson("/api/microservices/v3sctl-adapter/status"); - const v3sctlControlPlane = dockerCoreJson("/api/microservices/v3sctl-adapter/proxy/api/control-plane"); + const k3sctlStatus = dockerCoreJson("/api/microservices/k3sctl-adapter/status"); + const k3sctlControlPlane = dockerCoreJson("/api/microservices/k3sctl-adapter/proxy/api/control-plane"); const codeQueueStatus = dockerCoreJson("/api/microservices/code-queue/status"); const codeQueueHealth = dockerCoreJson("/api/microservices/code-queue/health"); const codeQueueTasks = dockerCoreJson("/api/microservices/code-queue/proxy/api/tasks/overview?limit=5&transcriptLimit=1&compact=1&afterSeq=0&preferId="); @@ -1106,7 +1106,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 const oaEventFlowStatsBody = (oaEventFlowStats as { body?: { ok?: boolean; stats?: unknown[]; returned?: number } }).body; const codeQueueHealthBody = (codeQueueHealth as { body?: { ok?: boolean; egressProxy?: { connected?: boolean }; queue?: { defaultModel?: string; judgeConfigured?: boolean; modelReasoningEfforts?: Record } } }).body; const codeQueueTasksBody = (codeQueueTasks as { body?: { ok?: boolean; queue?: { defaultModel?: string; modelReasoningEfforts?: Record }; tasks?: unknown[] } }).body; - const v3sctlControlPlaneBody = (v3sctlControlPlane as { body?: { + const k3sctlControlPlaneBody = (k3sctlControlPlane as { body?: { ok?: boolean; clusterId?: string; noFallback?: boolean; @@ -1123,8 +1123,8 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 instances?: Array<{ id?: string; healthy?: boolean; proxyMode?: string }>; }>; } }).body; - const v3sctlCodeQueueService = v3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue"); - const v3sctlD518Instance = v3sctlCodeQueueService?.instances?.find((instance) => instance.id === "D518"); + const k3sctlCodeQueueService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue"); + const k3sctlD518Instance = k3sctlCodeQueueService?.instances?.find((instance) => instance.id === "D518"); const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body; const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body; const filebrowserWebuiText = String((filebrowserWebui as { body?: { text?: string } }).body?.text || ""); @@ -1159,39 +1159,39 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 (microservices as { ok?: boolean }).ok === true && codeQueue?.providerId === "D601" && codeQueue.backend?.public === false - && codeQueue.backend?.proxyMode === "v3sctl-adapter-http" - && codeQueue.deployment?.mode === "v3sctl-managed" - && codeQueue.runtime?.orchestrator === "v3sctl" + && codeQueue.backend?.proxyMode === "k3sctl-adapter-http" + && codeQueue.deployment?.mode === "k3sctl-managed" + && codeQueue.runtime?.orchestrator === "k3sctl" && codeQueue.runtime?.container === null, { microservices }); - addSelectedCheck(checks, options, "microservice:v3sctl-adapter-status", - (v3sctlStatus as { ok?: boolean; body?: { microservice?: { id?: string; providerId?: string } } }).ok === true - && (v3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.id === "v3sctl-adapter" - && (v3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", - v3sctlStatus); - addSelectedCheck(checks, options, "microservice:v3sctl-control-plane", - (v3sctlControlPlane as { ok?: boolean }).ok === true - && v3sctlControlPlaneBody?.ok === true - && v3sctlControlPlaneBody.clusterId === "unidesk-v8s" - && v3sctlControlPlaneBody.noFallback === true - && v3sctlControlPlaneBody.managedServicesHealthy === true - && v3sctlControlPlaneBody.kubeApiProxy?.mode === "kubernetes-api-service-proxy" - && v3sctlCodeQueueService?.status === "healthy" - && v3sctlCodeQueueService?.topologyComplete === true - && v3sctlCodeQueueService?.servingHealthy === true - && v3sctlCodeQueueService?.active?.id === "D601" - && v3sctlCodeQueueService?.active?.healthy === true - && (v3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601") - && (v3sctlCodeQueueService?.presentNodeIds ?? []).includes("D518") - && (v3sctlCodeQueueService?.missingNodeIds ?? []).length === 0 - && v3sctlD518Instance?.healthy === true - && v3sctlD518Instance?.proxyMode === "kubernetes-api-pod-readiness", + addSelectedCheck(checks, options, "microservice:k3sctl-adapter-status", + (k3sctlStatus as { ok?: boolean; body?: { microservice?: { id?: string; providerId?: string } } }).ok === true + && (k3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.id === "k3sctl-adapter" + && (k3sctlStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === "D601", + k3sctlStatus); + addSelectedCheck(checks, options, "microservice:k3sctl-control-plane", + (k3sctlControlPlane as { ok?: boolean }).ok === true + && k3sctlControlPlaneBody?.ok === true + && k3sctlControlPlaneBody.clusterId === "unidesk-k3s" + && k3sctlControlPlaneBody.noFallback === true + && k3sctlControlPlaneBody.managedServicesHealthy === true + && k3sctlControlPlaneBody.kubeApiProxy?.mode === "kubernetes-api-service-proxy" + && k3sctlCodeQueueService?.status === "healthy" + && k3sctlCodeQueueService?.topologyComplete === true + && k3sctlCodeQueueService?.servingHealthy === true + && k3sctlCodeQueueService?.active?.id === "D601" + && k3sctlCodeQueueService?.active?.healthy === true + && (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601") + && (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D518") + && (k3sctlCodeQueueService?.missingNodeIds ?? []).length === 0 + && k3sctlD518Instance?.healthy === true + && k3sctlD518Instance?.proxyMode === "kubernetes-api-pod-readiness", { - ok: (v3sctlControlPlane as { ok?: boolean }).ok, - clusterId: v3sctlControlPlaneBody?.clusterId, - noFallback: v3sctlControlPlaneBody?.noFallback, - kubeApiProxy: v3sctlControlPlaneBody?.kubeApiProxy, - service: v3sctlCodeQueueService, + ok: (k3sctlControlPlane as { ok?: boolean }).ok, + clusterId: k3sctlControlPlaneBody?.clusterId, + noFallback: k3sctlControlPlaneBody?.noFallback, + kubeApiProxy: k3sctlControlPlaneBody?.kubeApiProxy, + service: k3sctlCodeQueueService, }); addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true && filebrowser?.providerId === "D518" diff --git a/src/components/backend-core/src/config.ts b/src/components/backend-core/src/config.ts index ad55a4a7..a8627265 100644 --- a/src/components/backend-core/src/config.ts +++ b/src/components/backend-core/src/config.ts @@ -80,8 +80,8 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon const frontend = asRecord(item.frontend, `${path}.frontend`); const deployment = item.deployment === undefined ? undefined : asRecord(item.deployment, `${path}.deployment`); const deploymentMode = deployment === undefined ? "unidesk-direct" : stringFromRecord(deployment, "mode", `${path}.deployment`); - if (deploymentMode !== "unidesk-direct" && deploymentMode !== "v3sctl-managed") { - throw new Error(`${path}.deployment.mode must be unidesk-direct or v3sctl-managed`); + if (deploymentMode !== "unidesk-direct" && deploymentMode !== "k3sctl-managed") { + throw new Error(`${path}.deployment.mode must be unidesk-direct or k3sctl-managed`); } return { id: stringFromRecord(item, "id", path), @@ -113,7 +113,7 @@ function parseMicroserviceConfig(value: unknown, index: number): MicroserviceCon : { mode: deploymentMode, adapterServiceId: optionalStringFromRecord(deployment, "adapterServiceId", `${path}.deployment`), - v3sServiceId: optionalStringFromRecord(deployment, "v3sServiceId", `${path}.deployment`), + k3sServiceId: optionalStringFromRecord(deployment, "k3sServiceId", `${path}.deployment`), namespace: optionalStringFromRecord(deployment, "namespace", `${path}.deployment`), expectedNodeIds: optionalStringArrayFromRecord(deployment, "expectedNodeIds", `${path}.deployment`), activeNodeId: optionalStringFromRecord(deployment, "activeNodeId", `${path}.deployment`), diff --git a/src/components/backend-core/src/microservice-proxy.ts b/src/components/backend-core/src/microservice-proxy.ts index 90c18e6c..af6518be 100644 --- a/src/components/backend-core/src/microservice-proxy.ts +++ b/src/components/backend-core/src/microservice-proxy.ts @@ -368,8 +368,8 @@ function canDirectProxyMicroservice(service: MicroserviceConfig): boolean { return service.providerId === "main-server"; } -function isV3sctlManagedMicroservice(service: MicroserviceConfig): boolean { - return service.deployment.mode === "v3sctl-managed" || service.backend.proxyMode === "v3sctl-adapter-http"; +function isK3sctlManagedMicroservice(service: MicroserviceConfig): boolean { + return service.deployment.mode === "k3sctl-managed" || service.backend.proxyMode === "k3sctl-adapter-http"; } // --------------------------------------------------------------------------- @@ -545,7 +545,7 @@ async function directMicroserviceResponse( } } -async function v3sctlAdapterMicroserviceResponse( +async function k3sctlAdapterMicroserviceResponse( service: MicroserviceConfig, method: string, targetPath: string, @@ -554,16 +554,16 @@ async function v3sctlAdapterMicroserviceResponse( bodyText: string, abortSignal?: AbortSignal, ): Promise { - const adapterServiceId = service.deployment.adapterServiceId ?? "v3sctl-adapter"; + const adapterServiceId = service.deployment.adapterServiceId ?? "k3sctl-adapter"; const adapter = microserviceById(adapterServiceId); if (adapter === null) { - return jsonResponse({ ok: false, error: `v3sctl adapter microservice not found: ${adapterServiceId}`, serviceId: service.id }, 502); + return jsonResponse({ ok: false, error: `k3sctl adapter microservice not found: ${adapterServiceId}`, serviceId: service.id }, 502); } - if (adapter.id === service.id || isV3sctlManagedMicroservice(adapter)) { - return jsonResponse({ ok: false, error: "v3sctl adapter must be a UniDesk-direct microservice", serviceId: service.id, adapterServiceId }, 500); + if (adapter.id === service.id || isK3sctlManagedMicroservice(adapter)) { + return jsonResponse({ ok: false, error: "k3sctl adapter must be a UniDesk-direct microservice", serviceId: service.id, adapterServiceId }, 500); } - const v3sServiceId = service.deployment.v3sServiceId ?? service.id; - const adapterTargetPath = `/api/services/${encodeURIComponent(v3sServiceId)}/proxy${targetPath}`; + const k3sServiceId = service.deployment.k3sServiceId ?? service.id; + const adapterTargetPath = `/api/services/${encodeURIComponent(k3sServiceId)}/proxy${targetPath}`; return fetchMicroserviceUpstreamResponse(adapter, method, adapterTargetPath, proxyOptions, requestHeaders, bodyText, abortSignal); } @@ -576,8 +576,8 @@ async function fetchMicroserviceUpstreamResponse( bodyText: string, abortSignal?: AbortSignal, ): Promise { - if (isV3sctlManagedMicroservice(service)) { - return v3sctlAdapterMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal); + if (isK3sctlManagedMicroservice(service)) { + return k3sctlAdapterMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal); } if (canDirectProxyMicroservice(service)) { return directMicroserviceResponse(service, method, targetPath, proxyOptions, requestHeaders, bodyText, abortSignal); @@ -716,12 +716,12 @@ export async function getMicroservices(): Promise { return config().microservices.map((service) => { const node = nodes.find((item) => item.providerId === service.providerId) ?? null; const docker = dockerStatuses.find((item) => item.providerId === service.providerId) ?? null; - const v3sManaged = isV3sctlManagedMicroservice(service); - const container = v3sManaged ? null : findContainer(docker?.dockerStatus ?? null, service.repository.containerName); + const k3sManaged = isK3sctlManagedMicroservice(service); + const container = k3sManaged ? null : findContainer(docker?.dockerStatus ?? null, service.repository.containerName); return { ...service, runtime: { - orchestrator: v3sManaged ? "v3sctl" : "unidesk-direct", + orchestrator: k3sManaged ? "k3sctl" : "unidesk-direct", providerStatus: node?.status ?? "missing", providerName: node?.name ?? service.providerId, providerLastHeartbeat: node?.lastHeartbeat ?? null, diff --git a/src/components/backend-core/src/types.ts b/src/components/backend-core/src/types.ts index 4fcee9bc..c6e38786 100644 --- a/src/components/backend-core/src/types.ts +++ b/src/components/backend-core/src/types.ts @@ -45,9 +45,9 @@ export interface MicroserviceConfig { timeoutMs: number; }; deployment: { - mode: "unidesk-direct" | "v3sctl-managed"; + mode: "unidesk-direct" | "k3sctl-managed"; adapterServiceId?: string; - v3sServiceId?: string; + k3sServiceId?: string; namespace?: string; expectedNodeIds?: string[]; activeNodeId?: string; diff --git a/src/components/frontend/public/app.js b/src/components/frontend/public/app.js index d1b9e2cc..4b0ad6e1 100644 --- a/src/components/frontend/public/app.js +++ b/src/components/frontend/public/app.js @@ -227,7 +227,7 @@ nav .material-icons::before { } } `;function f2({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return Vu("section",{className:`panel ${n||""}`},Vu("div",{className:"panel-head"},Vu("div",null,l?Vu("p",{className:"panel-eyebrow"},l):null,Vu(nl,{title:u,loading:i})),f?Vu("div",{className:"panel-actions"},f):null),Vu("div",{className:"panel-body"},r))}function ZZ({title:u,data:l,onOpen:f,testId:r}){return Vu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function OZ({title:u,text:l}){return Vu("div",{className:"empty-state"},Vu("strong",null,u),Vu("span",null,l))}function mQ(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function PQ(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function CQ(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function HZ(u){return u.filter((f)=>f?.id==="filebrowser"||String(f?.id||"").startsWith("filebrowser-")).sort((f,r)=>{let n=(i)=>i.providerId==="D518"?0:i.providerId==="D601"?1:i.id==="filebrowser"?2:3;return n(f)-n(r)||String(f.id).localeCompare(String(r.id))})}function BZ(u){if(u?.providerId==="D518")return"D518";return u?.providerId||u?.name||u?.id||"Unknown"}function VZ(u,l,f="/"){let r=f.startsWith("/")?f:`/${f}`;return`${u}/microservices/${encodeURIComponent(l)}/proxy${r}`}function DZ(u,l){return`${u}/microservices/${encodeURIComponent(l)}/health`}async function XZ(u,l=16000){let f=new AbortController,r=setTimeout(()=>f.abort(),l);try{return await Tu(u,{signal:f.signal,failureFields:[!1]})}finally{clearTimeout(r)}}function MQ(u){if(u?.providerId==="main-server")return"host / -> /srv";if(u?.providerId==="D601"||u?.providerId==="D518")return"WSL / + /mnt/c -> /srv";return"provider / -> /srv"}function a3(u){return u?.status==="OK"||u?.ok===!0}function SZ({service:u,active:l,health:f,onSelect:r,onRaw:n}){let i=mQ(u),y=PQ(u),t=CQ(u),_=i.container||{},c=a3(f?.body);return Vu("button",{type:"button",className:`filebrowser-target-card ${l?"active":""}`,"data-testid":`filebrowser-target-card-${u.id}`,onClick:r},Vu("span",{className:`status-badge ${c?"ok":i.providerStatus==="online"?"running":"warn"}`},c?"Health OK":i.providerStatus||"unknown"),Vu("strong",null,u.name||u.id),Vu("span",null,MQ(u)),Vu("code",null,`${y.nodeBindHost||"--"}:${y.nodePort||"--"}`),Vu("small",null,_.name?`${_.name} / ${_.state||"--"}`:`${t.composeService||"--"}`),Vu("span",{className:"filebrowser-card-raw",onClick:(A)=>{A.stopPropagation(),n(`${u.name} service`,u)}},"JSON"))}function RQ(u){try{return u?.contentDocument||u?.contentWindow?.document||null}catch{return null}}function n2(u){let l=RQ(u);if(l===null||l.head===null)return!1;let f=l.getElementById("unidesk-filebrowser-compact-style");if(f===null)f=l.createElement("style"),f.id="unidesk-filebrowser-compact-style",l.head.appendChild(f);if(f.textContent!==r2)f.textContent=r2;return!0}function YZ(u,l){let f=URL.createObjectURL(u),r=document.createElement("a");r.href=f,r.download=l,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(f),2000)}function pZ(u,l){let f=RQ(u);if(f===null||f.documentElement===null)throw Error("无法访问 File Browser iframe 文档");n2(u);let r=Math.max(640,Math.ceil(u.clientWidth||f.documentElement.clientWidth||1280)),n=Math.max(480,Math.ceil(u.clientHeight||f.documentElement.clientHeight||720)),i=f.documentElement.cloneNode(!0);i.querySelectorAll("script, style, link[rel='stylesheet'], link[rel='preload'], link[rel='icon']").forEach((A)=>A.remove()),i.querySelectorAll("img").forEach((A)=>{A.removeAttribute("src"),A.removeAttribute("srcset")});let y=i.querySelector("head");if(y===null)y=f.createElement("head"),i.insertBefore(y,i.firstChild);let t=f.createElement("style");t.textContent=`${r2} -html,body{width:${r}px!important;min-height:${n}px!important;overflow:hidden!important;}`,y.appendChild(t);let _=new XMLSerializer().serializeToString(i),c=`${_}`;YZ(new Blob([c],{type:"image/svg+xml;charset=utf-8"}),l.replace(/\.png$/i,".svg"))}function xQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=HZ(Array.isArray(u)?u:[]),n=new URLSearchParams(window.location.search).get("target")||"",i=n==="filebrowser-d518"?"filebrowser":n,y=r.some((Z)=>Z.id===i)?i:r[0]?.id||"",[t,_]=l2(y),[c,A]=l2({loading:!1,refreshedAt:null,health:{},error:""}),[j,F]=l2({exporting:!1,message:"",error:""}),J=EZ(null),Q=r.find((Z)=>Z.id===t)||r[0]||null,w=mQ(Q),L=PQ(Q),U=CQ(Q),N=Q?c.health[Q.id]:null,q=Q?VZ(f,Q.id,"/"):"about:blank";u2(()=>{if(r.length===0)return;if(!t||!r.some((Z)=>Z.id===t))_(r[0].id)},[r.map((Z)=>Z.id).join(",")]),u2(()=>{let Z=0,H=setInterval(()=>{if(Z+=1,n2(J.current)||Z>=24)clearInterval(H)},500);return()=>clearInterval(H)},[q]),u2(()=>{if(r.length===0)return;let Z=!1;async function H(){A((h)=>({...h,loading:!0,error:""}));let D=await Promise.all(r.map(async(h)=>{try{let V=await XZ(DZ(f,h.id));return[h.id,{ok:!0,body:V}]}catch(V){return[h.id,{ok:!1,error:Ou(V,"File Browser health failed")}]}}));if(Z)return;A({loading:!1,refreshedAt:new Date().toISOString(),health:Object.fromEntries(D),error:""})}H();let E=setInterval(H,30000);return()=>{Z=!0,clearInterval(E)}},[r.map((Z)=>`${Z.id}:${Z.runtime?.providerStatus||""}`).join(","),f]);function W(Z){_(Z);let H=new URL(window.location.href);H.searchParams.set("target",Z),window.history.replaceState({},"",`${H.pathname}${H.search}`)}async function z(){if(j.exporting)return;F({exporting:!0,message:"",error:""});try{let Z=new Date().toISOString().replace(/[-:.TZ]/g,"").slice(0,14);await pZ(J.current,`unidesk-filebrowser-${Q?.id||"target"}-${Z}.png`),F({exporting:!1,message:"截图已导出",error:""})}catch(Z){F({exporting:!1,message:"",error:Ou(Z,"截图导出失败")})}}if(r.length===0)return Vu(OZ,{title:"File Browser 未登记",text:"请在 config.json 的 microservices 中登记 id=filebrowser 或 filebrowser-* 用户服务"});return Vu("div",{className:"filebrowser-page","data-testid":"filebrowser-page"},c.error?Vu(il,{error:c.error,wide:!0}):null,Vu(f2,{title:"文件管理器",eyebrow:"File Browser / Host Files",loading:c.loading,actions:Vu("div",{className:"panel-actions"},Q?Vu("button",{type:"button",className:"ghost-btn",onClick:z,disabled:j.exporting,"data-testid":"filebrowser-export-screenshot"},j.exporting?"导出中...":"导出截图"):null,Q?Vu("a",{className:"ghost-btn",href:q,target:"_blank",rel:"noreferrer"},"新窗口打开"):null,Q?Vu(ZZ,{title:"File Browser 当前目标",data:{service:Q,health:N},onOpen:l,testId:"raw-filebrowser-active"}):null)},Vu("div",{className:"filebrowser-hero"},Vu("div",null,Vu("span",{className:`status-badge ${a3(N?.body)?"ok":"warn"}`},a3(N?.body)?"Health OK":"Health Pending"),Vu("h3",null,Q?.name||"File Browser"),Vu("p",{className:"muted paragraph"},Q?.description||"通过 UniDesk 登录态代理访问,不开放 File Browser 公网端口。"),j.error?Vu("p",{className:"filebrowser-shot-error"},j.error):null,j.message?Vu("p",{className:"filebrowser-shot-ok"},j.message):null),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Provider"),Vu("strong",null,Q?.providerId||"--"),Vu("code",null,w.providerName||Q?.providerId||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Private Backend"),Vu("strong",null,`${L.nodeBindHost||"--"}:${L.nodePort||"--"}`),Vu("code",null,L.nodeBaseUrl||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Image"),Vu("strong",null,U.dockerfile||"filebrowser/filebrowser:v2.63.3"),Vu("code",null,U.commitId||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Mount"),Vu("strong",null,MQ(Q)),Vu("code",null,Q?.providerId==="main-server"?"/root, /var, /home":"/home, /mnt/c, /mnt/d")))),Vu(f2,{title:"浏览目标",eyebrow:`${r.length} host targets`,loading:c.loading},Vu("div",{className:"filebrowser-target-grid"},r.map((Z)=>Vu(SZ,{key:Z.id,service:Z,active:Z.id===Q?.id,health:c.health[Z.id],onSelect:()=>W(Z.id),onRaw:l})))),Vu(f2,{title:`${BZ(Q)} 文件视图`,eyebrow:N?.body?`Health ${a3(N.body)?"OK":"UNKNOWN"} / ${c.refreshedAt?tl(c.refreshedAt):"--"}`:"Embedded WebUI",className:"filebrowser-frame-panel"},Vu("div",{className:"filebrowser-frame-shell"},Vu("div",{className:"filebrowser-frame-toolbar"},Vu("span",null,"BaseURL"),Vu("code",null,`/api/microservices/${Q?.id||"filebrowser"}/proxy`),Vu("span",null,"Root"),Vu("code",null,"/srv"),Vu("span",{className:"filebrowser-compact-note"},"Compact layout injected")),Vu("iframe",{ref:J,key:q,title:`${Q?.name||"File Browser"} WebUI`,src:q,className:"filebrowser-frame","data-testid":"filebrowser-frame",onLoad:(Z)=>n2(Z.currentTarget),sandbox:"allow-downloads allow-forms allow-modals allow-same-origin allow-scripts"}))))}var uc=Pu(Jl(),1);var Uu=uc.default.createElement,{useEffect:mZ}=uc.default,PZ=uc.default.useState;function o3({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return Uu("span",{className:`status-badge ${f}`},l||u||"unknown")}function On({label:u,value:l,hint:f,tone:r}){return Uu("article",{className:`metric-card ${r||""}`},Uu("div",{className:"metric-label"},u),Uu("div",{className:"metric-value"},l),Uu("div",{className:"metric-hint"},f))}function d3({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return Uu("section",{className:`panel ${n||""}`},Uu("div",{className:"panel-head"},Uu("div",null,l?Uu("p",{className:"panel-eyebrow"},l):null,Uu(nl,{title:u,loading:i})),f?Uu("div",{className:"panel-actions"},f):null),Uu("div",{className:"panel-body"},r))}function e3({title:u,data:l,onOpen:f,testId:r}){return Uu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function i2({title:u,text:l}){return Uu("div",{className:"empty-state"},Uu("strong",null,u),Uu("span",null,l))}function CZ(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function MZ(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function RZ(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function Ei(u,l){let f=u&&typeof u==="object"?u[l]:void 0;return Number.isFinite(Number(f))?String(f):"--"}function xZ(u){return(Array.isArray(u?.jobs)?u.jobs:[]).slice(0,40)}function hZ(u){return(Array.isArray(u?.drafts)?u.drafts:[]).slice(0,12)}function hQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((Q)=>Q.id==="findjob")||null,[n,i]=PZ({loading:!1,error:"",health:null,summary:null,jobs:null,drafts:null,refreshedAt:null});async function y(){if(!r)return;i((Q)=>({...Q,loading:!0,error:""}));try{let[Q,w,L,U]=await Promise.all([Tu(`${f}/microservices/findjob/health`),Tu(`${f}/microservices/findjob/proxy/api/summary`),Tu(`${f}/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:40`),Tu(`${f}/microservices/findjob/proxy/api/drafts`)]);i({loading:!1,error:"",health:Q,summary:w,jobs:L,drafts:U,refreshedAt:new Date})}catch(Q){i((w)=>({...w,loading:!1,error:Ou(Q,"FindJob 加载失败")}))}}if(mZ(()=>{y()},[r?.id,r?.runtime?.providerStatus]),!r)return Uu(i2,{title:"FindJob 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=findjob"});let t=CZ(r),_=RZ(r),c=MZ(r),A=n.summary||{},j=xZ(n.jobs),F=hZ(n.drafts),J=n.jobs?._unidesk?.arrayLimits?.jobs;return Uu("div",{className:"findjob-page","data-testid":"findjob-page"},Uu(d3,{title:"FindJob 工作台",eyebrow:"D601 用户服务",loading:n.loading,actions:Uu("div",{className:"panel-actions"},Uu("button",{type:"button",className:"ghost-btn",onClick:y,disabled:n.loading,"data-testid":"findjob-refresh-button"},n.loading?"刷新中":"刷新"),Uu(e3,{title:"FindJob 用户服务",data:r,onOpen:l,testId:"raw-findjob-service"}))},Uu("div",{className:"findjob-hero"},Uu("div",null,Uu("div",{className:"node-version-line"},Uu(o3,{status:t.providerStatus==="online"?"online":"warn"},t.providerStatus||"unknown"),Uu("span",null,r.providerId),Uu("span",null,c.public?"公网暴露":"仅 UniDesk frontend 代理访问")),Uu("p",{className:"muted paragraph"},r.description)),Uu("div",{className:"microservice-ref-card"},Uu("span",null,"Repo"),Uu("strong",null,_.url||"--"),Uu("code",null,_.commitId||"--")),Uu("div",{className:"microservice-ref-card"},Uu("span",null,"D601 Docker"),Uu("strong",null,`${c.nodeBindHost||"--"}:${c.nodePort||"--"}`),Uu("code",null,`${_.composeFile||"--"} / ${_.composeService||"--"}`))),Uu(il,{error:n.error,wide:!0})),Uu("div",{className:"findjob-grid"},Uu(d3,{title:"岗位指标",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Summary",loading:n.loading},Uu("div",{className:"metric-grid"},Uu(On,{label:"岗位总量",value:Ei(A,"totalJobs"),hint:"tracked jobs",tone:"ok"}),Uu(On,{label:"原始岗位",value:Ei(A,"rawJobs"),hint:"raw queue"}),Uu(On,{label:"已验证",value:Ei(A,"verifiedJobs"),hint:"verified set"}),Uu(On,{label:"优先处理",value:Ei(A,"prioritizedJobs"),hint:"prioritized"}),Uu(On,{label:"过期",value:Ei(A,"staleJobs"),hint:"stale jobs",tone:"warn"}),Uu(On,{label:"无效",value:Ei(A,"invalidJobs"),hint:"invalid jobs",tone:"warn"}),Uu(On,{label:"上海",value:Ei(A,"shanghaiJobs"),hint:"city filter"}),Uu(On,{label:"Health",value:n.health?.ok?"OK":"--",hint:"D601 /api/health"})),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Summary",data:A,onOpen:l,testId:"raw-findjob-summary"}))),Uu(d3,{title:"近期岗位",eyebrow:J?`${J.returnedLength}/${J.originalLength} Preview`:`${j.length} Preview`,loading:n.loading},j.length===0?Uu(i2,{title:"暂无岗位预览",text:"等待 D601 findjob backend 返回 /api/jobs"}):Uu("div",{className:"table-wrap findjob-job-table"},Uu("table",null,Uu("thead",null,Uu("tr",null,Uu("th",null,"优先级"),Uu("th",null,"状态"),Uu("th",null,"单位"),Uu("th",null,"职位"),Uu("th",null,"城市"),Uu("th",null,"阶段"),Uu("th",null,"截止"),Uu("th",null,"证据"))),Uu("tbody",null,j.map((Q)=>Uu("tr",{key:Q.id},Uu("td",null,Uu(o3,{status:String(Q.priority||"").toLowerCase()||"unknown"},Q.priority||"--")),Uu("td",null,Uu(o3,{status:String(Q.status||"").toLowerCase()||"unknown"},Q.status||"--")),Uu("td",null,Q.organization_name||"--",Uu("code",null,Q.id||"--")),Uu("td",null,Q.display_title||Q.title||"--"),Uu("td",null,Q.display_city||Q.city||"--"),Uu("td",null,Q.workflow_stage||"--"),Uu("td",null,Q.deadline||"--"),Uu("td",null,Q.evidence_url?Uu("a",{href:Q.evidence_url,target:"_blank",rel:"noreferrer"},"打开"):Uu("span",{className:"muted"},"无"))))))),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Jobs Preview",data:n.jobs,onOpen:l,testId:"raw-findjob-jobs"}))),Uu(d3,{title:"草稿与报告",eyebrow:`${F.length} Drafts`,loading:n.loading},F.length===0?Uu(i2,{title:"暂无草稿",text:"D601 findjob backend 未返回 drafts"}):Uu("div",{className:"draft-list"},F.map((Q)=>Uu("article",{key:Q.id,className:"draft-card"},Uu("div",{className:"node-card-head"},Uu("strong",null,Q.id),Uu(o3,{status:Q.status},Q.status||"--")),Uu("div",{className:"docker-meta compact"},Uu("span",null,Q.workflow_stage||"--"),Uu("span",null,`jobs ${Q.counts?.jobs??0}`),Uu("span",null,`reports ${Q.counts?.reports??0}`)),Uu("span",null,Q.latestReportPath||"暂无报告"),Uu("code",null,qu(Q.updated_at||Q.updatedAt))))),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Drafts",data:n.drafts,onOpen:l,testId:"raw-findjob-drafts"})))))}var gt=Pu(Jl(),1);var M=gt.default.createElement,{useEffect:bZ}=gt.default,y2=gt.default.useState;function vt(u){let l=Number(u);return Number.isFinite(l)?`${Math.max(0,Math.min(100,l)).toFixed(1)}%`:"--"}function _2(u){if(u===null||u===void 0||u==="")return"--";let l=Number(u);if(!Number.isFinite(l))return"--";if(l<60)return`${Math.max(0,Math.round(l))}s`;if(l<3600)return`${Math.floor(l/60)}m ${Math.round(l%60)}s`;return`${Math.floor(l/3600)}h ${Math.floor(l%3600/60)}m`}function $2(u,l=2){let f=Number(u);if(!Number.isFinite(f))return u===!1?"false":u===!0?"true":"--";let r=Math.abs(f);if(Number.isInteger(f)||r>=1000)return f.toLocaleString("zh-CN",{maximumFractionDigits:0});if(r>=1)return f.toLocaleString("zh-CN",{maximumFractionDigits:l});return f.toLocaleString("zh-CN",{maximumFractionDigits:Math.max(l,6)})}function It(u){if(u===null||u===void 0||u==="")return"--";if(typeof u==="boolean")return u?"true":"false";if(typeof u==="number")return $2(u,4);if(Array.isArray(u))return u.map((l)=>It(l)).join(" x ");if(typeof u==="object")return"已上报";return String(u)}function lc(u){let l=Number(u);if(!Number.isFinite(l)||l<=0)return"--";let f=l>=100?0:l>=10?1:2;return`${l.toLocaleString("zh-CN",{maximumFractionDigits:f})} epoch/h`}function fc(u){return u.replace(/[^a-zA-Z0-9_-]/g,"-")}function If(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function kt({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return M("span",{className:`status-badge ${f}`},l||u||"unknown")}function Hn({label:u,value:l,hint:f,tone:r}){return M("article",{className:`metric-card ${r||""}`},M("div",{className:"metric-label"},u),M("div",{className:"metric-value"},l),M("div",{className:"metric-hint"},f))}function t2({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return M("section",{className:`panel ${n||""}`},M("div",{className:"panel-head"},M("div",null,l?M("p",{className:"panel-eyebrow"},l):null,M(nl,{title:u,loading:i})),f?M("div",{className:"panel-actions"},f):null),M("div",{className:"panel-body"},r))}function v1({title:u,data:l,onOpen:f,testId:r}){return M("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:(n)=>{n?.stopPropagation?.(),f(u,l)}},"查看原始JSON")}function p0({title:u,text:l}){return M("div",{className:"empty-state"},M("strong",null,u),M("span",null,l))}function vZ(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function kZ(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function IZ(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function gZ(u){return u?.counts&&typeof u.counts==="object"&&!Array.isArray(u.counts)?u.counts:{}}function sZ(u){return Array.isArray(u?.jobs)?u.jobs.slice(0,240):[]}function aZ(u){return Array.isArray(u?.projects)?u.projects.slice(0,1000):[]}function rc(u){return Array.isArray(u?.projects)?u.projects:[]}function oZ(u,l){if(Array.isArray(l?.gpu))return l.gpu;if(Array.isArray(u?.gpu))return u.gpu;return[]}function Fr(u,l){return`${u}/microservices/met-nonlinear/proxy${l}`}function bQ(u){return u.startedAt&&u.finishedAt?_2((Date.parse(u.finishedAt)-Date.parse(u.startedAt))/1000):"--"}function dZ(u){let l=u.progress||{};if(l.etaSeconds!==null&&l.etaSeconds!==void 0&&l.etaSeconds!==""){let y=Number(l.etaSeconds);if(Number.isFinite(y))return Math.max(0,y)}let f=Number(l.currentEpoch),r=Number(l.epochTarget??u.epochTarget),n=Date.parse(u.startedAt||"");if(!Number.isFinite(f)||f<=0||!Number.isFinite(r)||r<=f||!Number.isFinite(n))return null;let i=Math.max(0,(Date.now()-n)/1000);if(i<=0)return null;return Math.max(0,i/f*(r-f))}function vQ(u){let l=u.progress||{},f=Number(l.epochPerHour);if(Number.isFinite(f)&&f>0)return f;let r=Date.parse(u.startedAt||""),n=["succeeded","failed","canceled"].includes(u.status)?Date.parse(u.finishedAt||""):Date.now();if(!Number.isFinite(r)||!Number.isFinite(n)||n<=r)return null;let i=Number(l.currentEpoch??u.epochTarget);if(!Number.isFinite(i)||i<=0)return null;return i/((n-r)/3600000)}function kQ(u){if(u==="staged")return"待启动";if(u==="queued")return"排队中";if(u==="running")return"训练中";if(u==="succeeded")return"已完成";if(u==="failed")return"失败";if(u==="canceled")return"已取消";return u||"unknown"}function IQ(u,l,f){return{name:u,path:l,depth:f,count:0,children:[],project:null}}function eZ(u){let l=IQ("","",-1);for(let r of u){let i=String(r?.projectPath||"").replace(/\\/g,"/").split("/").filter(Boolean);if(i.length===0)continue;let y=l,t=[];for(let[_,c]of i.entries()){t.push(c);let A=t.join("/"),j=y.children.find((F)=>F.path===A);if(!j)j=IQ(c,A,_),y.children.push(j);if(_===i.length-1)j.project=r;y=j}}let f=(r)=>{let n=r.children.reduce((i,y)=>i+f(y),0);return r.count=(r.project?1:0)+n,r.children.sort((i,y)=>{if(Boolean(i.project)!==Boolean(y.project))return i.project?1:-1;return i.name.localeCompare(y.name,"zh-CN",{numeric:!0,sensitivity:"base"})}),r.count};return f(l),l}function uO(u){let l=If(u.data);return If(l.project).projectPath?If(l.project):l}function lO(u){return If(If(u.data).job)}function gQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((P)=>P.id==="met-nonlinear")||null,[n,i]=y2({loading:!1,actionBusy:!1,error:"",health:null,summary:null,queue:null,projects:null,history:null,images:null,refreshedAt:null}),[y,t]=y2({loading:!1,error:"",kind:"",key:"",title:"",data:null}),[_,c]=y2(()=>({activeTab:"projects",selectedProjects:{},expandedProjectDirs:{},sourceProject:"",forkCount:1,forkEpochs:200,forkPrefix:`ui_fork_${Date.now()}`,maxConcurrency:3,targetGpuName:"2080 Ti",actionMessage:""}));function A(P){c((e)=>({...e,...P}))}async function j(P=_.activeTab){if(!r)return;i((e)=>({...e,loading:!0,error:""}));try{let e=[["health",Tu(`${f}/microservices/met-nonlinear/health`)],["summary",Tu(Fr(f,"/api/summary"))]];if(P==="projects")e.push(["projectsRoot",Tu(Fr(f,"/api/projects?root=projects&limit=500"))]),e.push(["exProjectsRoot",Tu(Fr(f,"/api/projects?root=ex_projects&limit=500"))]);if(P==="current"||P==="completed"||P==="failed")e.push(["queue",Tu(Fr(f,"/api/queue"))]);if(P==="completed"||P==="failed")e.push(["history",Tu(Fr(f,"/api/history"))]);if(P==="gpu")e.push(["images",Tu(Fr(f,"/api/images"))]);let uu=Object.fromEntries(await Promise.all(e.map(async([s,Nu])=>[s,await Nu]))),Ku={loading:!1,actionBusy:!1,error:"",health:uu.health,summary:uu.summary,refreshedAt:new Date};if(uu.projectsRoot||uu.exProjectsRoot){let{projectsRoot:s,exProjectsRoot:Nu}=uu;Ku.projects={ok:s?.ok!==!1&&Nu?.ok!==!1,roots:[{root:"projects",count:rc(s).length},{root:"ex_projects",count:rc(Nu).length}],projects:[...rc(s),...rc(Nu)]}}if(uu.queue)Ku.queue=uu.queue;if(uu.history)Ku.history=uu.history;if(uu.images)Ku.images=uu.images;i((s)=>({...s,...Ku}))}catch(e){i((uu)=>({...uu,loading:!1,actionBusy:!1,error:Ou(e,"MET Nonlinear 加载失败")}))}}async function F(P,e){i((uu)=>({...uu,actionBusy:!0,error:""})),A({actionMessage:`${P}...`});try{let uu=await e();A({actionMessage:uu||`${P}完成`}),await j()}catch(uu){i((Ku)=>({...Ku,actionBusy:!1,error:Ou(uu,`${P}失败`)}))}}async function J(){await F("保存并发设置",async()=>{await Tu(Fr(f,"/api/queue/settings"),{method:"PUT",body:JSON.stringify({maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName})})})}function Q(){return Object.entries(_.selectedProjects).filter(([,P])=>P).map(([P])=>P)}async function w(){let P=Q();if(P.length===0)throw Error("请先选择至少一个 project");await F("加入待启动队列",async()=>{await Tu(Fr(f,"/api/queue"),{method:"POST",body:JSON.stringify({projectPaths:P,maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName,start:!1})}),A({activeTab:"current",selectedProjects:{}})})}async function L(){let P=_.sourceProject||p[0]?.projectPath;if(!P)throw Error("请先选择源 project");await F("Fork Project",async()=>{let e=await Tu(Fr(f,"/api/projects/fork"),{method:"POST",body:JSON.stringify({sourceProject:P,count:Number(_.forkCount),epochs:Number(_.forkEpochs),prefix:_.forkPrefix})}),uu=Array.isArray(e.projectPaths)?e.projectPaths:[],Ku=uu.reduce((s,Nu)=>{return s[Nu]=!0,s},{..._.selectedProjects});return A({selectedProjects:Ku}),`已 fork ${uu.length} 个 project,并已自动勾选;请确认后点击加入待启动队列。`})}async function U(){await F("启动队列",async()=>{await Tu(Fr(f,"/api/queue/start"),{method:"POST",body:JSON.stringify({maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName})}),A({activeTab:"current"})})}async function N(P){await F("取消任务",async()=>{await Tu(Fr(f,`/api/jobs/${encodeURIComponent(P.id)}/cancel`),{method:"POST",body:JSON.stringify({})})})}async function q(P){let e=String(P?.projectPath||"");if(!e)return;t({loading:!0,error:"",kind:"project",key:e,title:e,data:null});try{let uu=await Tu(Fr(f,`/api/projects/config?path=${encodeURIComponent(e)}`));t({loading:!1,error:"",kind:"project",key:e,title:e,data:uu})}catch(uu){t({loading:!1,error:Ou(uu,"Project 详情加载失败"),kind:"project",key:e,title:e,data:null})}}async function W(P){let e=String(P?.id||"");if(!e)return;t({loading:!0,error:"",kind:"job",key:e,title:P.projectPath||e,data:null});try{let uu=await Tu(Fr(f,`/api/jobs/${encodeURIComponent(e)}`));t({loading:!1,error:"",kind:"job",key:e,title:uu?.job?.projectPath||P.projectPath||e,data:uu})}catch(uu){t({loading:!1,error:Ou(uu,"Job 详情加载失败"),kind:"job",key:e,title:P.projectPath||e,data:null})}}if(bZ(()=>{j(_.activeTab)},[r?.id,r?.runtime?.providerStatus,_.activeTab]),!r)return M(p0,{title:"MET Nonlinear 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=met-nonlinear"});let z=vZ(r),Z=IZ(r),H=kZ(r),E=gZ(n.queue?.queue||n.summary?.queue),D=oZ(n.health,n.queue),h=n.health?.targetGpu||n.summary?.targetGpu||D.find((P)=>String(P.name||"").includes("2080")),V=n.images?.mlImage||n.health?.image||{},S=sZ(n.queue),p=aZ(n.projects),O=eZ(p),m=_.sourceProject||p[0]?.projectPath||"",X=S.filter((P)=>["staged","queued","running"].includes(P.status)),v=S.filter((P)=>P.status==="succeeded"),T=S.filter((P)=>["failed","canceled"].includes(P.status)),Y=Array.isArray(n.history?.jobs)?n.history.jobs.slice(0,120):[],k=[{id:"projects",label:"项目库",count:p.length},{id:"current",label:"当前队列",count:X.length||Number(E.staged||0)+Number(E.queued||0)+Number(E.running||0)},{id:"completed",label:"已完成",count:v.length||Number(E.succeeded||0)},{id:"failed",label:"失败诊断",count:T.length||Number(E.failed||0)+Number(E.canceled||0)},{id:"gpu",label:"GPU/镜像",count:D.length}];function I(P,e){if(P.length===0)return M(p0,{title:e==="current"?"当前队列为空":"暂无记录",text:e==="current"?"从项目库选择或 fork project 后先加入待启动队列,再启动队列。":"终态任务会显示耗时、exit code 和失败诊断。"});return M("div",{className:"table-wrap met-job-table"},M("table",null,M("thead",null,M("tr",null,M("th",null,"状态"),M("th",null,"Project"),M("th",null,"Epoch"),M("th",null,"速度"),M("th",null,"ETA/耗时"),M("th",null,"GPU"),M("th",null,"Exit"),M("th",null,"更新时间"),M("th",null,"操作"))),M("tbody",null,P.map((uu)=>{let Ku=uu.progress||{},s=["staged","queued","running"].includes(uu.status),Nu=y.kind==="job"&&y.key===uu.id;return M("tr",{key:uu.id,className:`met-click-row ${Nu?"active":""}`,onClick:()=>W(uu),"data-testid":`met-job-row-${fc(uu.id)}`},M("td",null,M(kt,{status:uu.status},kQ(uu.status))),M("td",null,M("button",{type:"button",className:"met-inline-link",onClick:(Eu)=>{Eu.stopPropagation(),W(uu)}},uu.projectPath),M("code",null,uu.id)),M("td",null,M("span",null,`${Ku.currentEpoch??"--"} / ${Ku.epochTarget??uu.epochTarget??"--"}`),M("div",{className:"met-progress"},M("span",{style:{width:vt(Ku.progressPercent)}}))),M("td",null,M("strong",null,lc(vQ(uu)))),M("td",null,uu.status==="succeeded"||uu.status==="failed"||uu.status==="canceled"?bQ(uu):uu.status==="running"?`ETA ${_2(dZ(uu))}`:"--"),M("td",null,uu.gpuName||"--"),M("td",null,uu.exitCode??"--"),M("td",null,qu(uu.updatedAt)),M("td",null,s?M("button",{type:"button",className:"ghost-btn mini",onClick:(Eu)=>{Eu.stopPropagation(),N(uu)},disabled:n.actionBusy},"取消"):null,M(v1,{title:`MET Job ${uu.id}`,data:uu,onOpen:l,testId:`raw-met-job-${uu.id}`})))}))))}function b(){return M("div",{className:"met-queue-summary","data-testid":"met-current-summary"},M(kt,{status:"staged"},`待启动 ${E.staged??0}`),M(kt,{status:"queued"},`排队中 ${E.queued??0}`),M(kt,{status:"running"},`训练中 ${E.running??0}`),M("span",null,`最大并发 ${n.summary?.queue?.maxConcurrency??n.queue?.queue?.maxConcurrency??_.maxConcurrency}`),M("span",null,`目标 GPU ${n.summary?.queue?.targetGpuName??n.queue?.queue?.targetGpuName??_.targetGpuName}`))}function o(P,e){let uu=_.expandedProjectDirs[P];return uu===void 0?e<2:Boolean(uu)}function g(P,e){let uu=o(P,e);A({expandedProjectDirs:{..._.expandedProjectDirs,[P]:!uu}})}function x(P){let e=8+Math.max(0,P.depth)*16;if(Boolean(P.project)){let s=P.project,Nu=Boolean(_.selectedProjects[s.projectPath]),Eu=y.kind==="project"&&y.key===s.projectPath;return M("div",{key:P.path,className:`met-tree-row project ${Nu?"selected":""} ${Eu?"active":""}`,style:{paddingLeft:e},onClick:()=>q(s),"data-testid":`met-project-node-${fc(s.projectPath)}`},M("div",{className:"met-tree-name"},M("input",{type:"checkbox",checked:Nu,onClick:(Hu)=>Hu.stopPropagation(),onChange:(Hu)=>A({selectedProjects:{..._.selectedProjects,[s.projectPath]:Hu.target.checked}}),"data-testid":`met-project-checkbox-${fc(s.projectPath)}`}),M("button",{type:"button",className:"met-inline-link project-path",onClick:(Hu)=>{Hu.stopPropagation(),q(s)}},P.name)),M("span",null,s.useModel||"--"),M("span",null,s.epochTrain??"--"),M("span",null,vt(s.progress?.progressPercent)),M("span",null,lc(s.progress?.epochPerHour)))}let Ku=o(P.path,P.depth);return M(gt.default.Fragment,{key:P.path},M("div",{className:"met-tree-row folder",style:{paddingLeft:e},"data-testid":`met-project-folder-${fc(P.path)}`},M("button",{type:"button",className:"met-tree-toggle",onClick:()=>g(P.path,P.depth),"aria-label":Ku?`折叠 ${P.path}`:`展开 ${P.path}`},Ku?"-":"+"),M("strong",null,P.name),M("span",{className:"met-tree-count"},`${P.count} projects`)),Ku?P.children.map((s)=>x(s)):null)}function lu(P){return M("div",{className:"met-detail-kv"},P.map((e)=>M("div",{key:e.label,className:"met-detail-kv-item"},M("span",null,e.label),M("strong",null,It(e.value)),e.hint?M("small",null,e.hint):null)))}function _u(P,e){return M("div",{className:"met-detail-section"},M("h3",null,P),lu(e))}function $u(P){if(!Array.isArray(P)||P.length===0)return M(p0,{title:"模型层未上报",text:"等待 data/model_info.json 或 compute_analysis.json 生成。"});return M("div",{className:"table-wrap met-layer-table"},M("table",null,M("thead",null,M("tr",null,M("th",null,"Layer"),M("th",null,"Type"),M("th",null,"Params"),M("th",null,"Trainable"),M("th",null,"Compute"))),M("tbody",null,P.slice(0,18).map((e,uu)=>M("tr",{key:`${e.name||"layer"}-${uu}`},M("td",null,e.name||`#${uu+1}`),M("td",null,e.type||"--"),M("td",null,$2(e.num_params)),M("td",null,e.trainable===void 0?"--":String(Boolean(e.trainable))),M("td",null,$2(e.compute?.total??e.estimated_cost?.weighted_units?.total)))))))}function ju(P){let e=Array.isArray(P)?P:[];if(e.length===0)return M(p0,{title:"data/ 暂无文件",text:"训练或评估完成后会生成 training_state、metrics、model_info 等文件。"});return M("div",{className:"met-file-chip-grid"},e.slice(0,48).map((uu)=>M("span",{key:uu},uu)),e.length>48?M("span",null,`+${e.length-48}`):null)}function zu(P){let e=String(P||"").replace(/\x1b\[[0-9;]*[A-Za-z]/g,"").split(/\r?\n/).map((uu)=>uu.trim()).filter(Boolean).slice(-12);if(e.length===0)return M(p0,{title:"暂无日志尾部",text:"该任务未上报 logTail 或日志已轮转。"});return M("div",{className:"met-log-lines"},e.map((uu,Ku)=>M("div",{key:`${Ku}-${uu.slice(0,16)}`},uu)))}function Wu(){if(y.loading)return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},"Detail Loading"),M(nl,{title:"详情加载中",loading:!0}))),M(p0,{title:"详情加载中",text:y.title||"正在读取 D601 data/ 和 config.json"}));if(y.error)return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M(il,{error:y.error,wide:!0}));if(!y.data)return M("section",{className:"met-detail-panel muted","data-testid":"met-detail-panel"},M(p0,{title:"选择一个项目或任务查看详情",text:"项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。"}));let P=uO(y),e=lO(y),uu=If(P.config),Ku=If(P.progress||e.progress),s=If(P.data),Nu=If(P.metrics||s.metrics||Ku.trainingInfo?.evaluation_metrics),Eu=If(s.trainingInfo||Ku.trainingInfo),Hu=If(s.trainingState),vu=If(P.model||s.model),ul=Array.isArray(vu.modelSummary)&&vu.modelSummary.length>0?vu.modelSummary:vu.computeLayers,mu=If(Eu.evaluation_metrics),Fl=y.kind==="job"?"训练任务详情":"Project 详情";return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},y.kind==="job"?"Job + Project Detail":"Project Library Detail"),M(nl,{title:Fl}),M("code",null,P.projectPath||e.projectPath||y.title)),M("div",{className:"panel-actions"},M(v1,{title:`MET ${Fl}`,data:y.data,onOpen:l,testId:"raw-met-detail"}))),y.kind==="job"?_u("任务状态",[{label:"Job ID",value:e.id},{label:"状态",value:kQ(e.status)},{label:"GPU",value:e.gpuName},{label:"Exit Code",value:e.exitCode},{label:"耗时",value:bQ(e)},{label:"训练速度",value:lc(vQ({...e,progress:Ku}))}]):null,_u("config.json",[{label:"use_model",value:uu.use_model},{label:"epoch_train",value:uu.epoch_train},{label:"step_per_epoch",value:uu.step_per_epoch},{label:"learning_rate",value:uu.learning_rate},{label:"using_gpu",value:uu.using_gpu},{label:"use_points",value:uu.use_points},{label:"sample_rate",value:uu.sample_rate},{label:"time_clipped_s",value:uu.time_clipped_s},{label:"H_UNITS",value:uu.H_UNITS},{label:"INNER_KAN_UNITS",value:uu.INNER_KAN_UNITS},{label:"INNER_KAN_LAYERS",value:uu.INNER_KAN_LAYERS},{label:"GRID_SIZE",value:uu.GRID_SIZE},{label:"SPLINE_ORDER",value:uu.SPLINE_ORDER},{label:"USE_FAST_MODEL",value:uu.USE_FAST_MODEL},{label:"IIR_TRAINABLE",value:uu.IIR_TRAINABLE}]),_u("data/ 训练状态",[{label:"Epoch",value:`${Ku.currentEpoch??Hu.current_epoch??Hu.completed_epoch??"--"} / ${Ku.epochTarget??uu.epoch_train??"--"}`},{label:"Progress",value:vt(Ku.progressPercent)},{label:"Last Loss",value:Ku.lastLoss??Hu.loss},{label:"Last Val Loss",value:Ku.lastValLoss??Hu.val_loss},{label:"Min Loss",value:Eu.min_loss??Hu.min_loss},{label:"Min Val Loss",value:Eu.min_val_loss??Hu.min_val_loss},{label:"Log Lines",value:Ku.logLineCount},{label:"ETA",value:_2(Ku.etaSeconds??Hu.remaining_time)},{label:"训练速度",value:lc(Ku.epochPerHour??Hu.smoothed_speed)},{label:"Training Alive",value:Hu.training_alive}]),_u("模型参数",[{label:"Model Type",value:vu.modelType??uu.use_model},{label:"Total Params",value:vu.totalParams,hint:vu.totalParams===null||vu.totalParams===void 0?"未上报":"data/model_info.json"},{label:"Trainable",value:vu.trainableParams},{label:"Non-trainable",value:vu.nonTrainableParams},{label:"Compute Cost",value:vu.computeCost},{label:"Estimate Status",value:vu.estimateStatus},{label:"Unsupported Layers",value:vu.unsupportedLayerCount}]),_u("指标",[{label:"train_loss",value:Nu.train_loss??mu.train_loss},{label:"val_loss",value:Nu.val_loss??mu.val_loss},{label:"train_mae",value:Nu.train_mae??mu.train_mae},{label:"val_mae",value:Nu.val_mae??mu.val_mae},{label:"train_afmae",value:Nu.train_afmae??mu.train_afmae},{label:"val_afmae",value:Nu.val_afmae??mu.val_afmae},{label:"freq_drift_hz",value:Nu.freq_drift_hz},{label:"sens_drift_percent",value:Nu.sens_drift_percent},{label:"linearity_percent",value:Nu.linearity_percent},{label:"weights_source",value:Nu.weights_source??mu.weights_source},{label:"lr min/mean/max",value:`${It(Eu.learning_rate_min)} / ${It(Eu.learning_rate_mean)} / ${It(Eu.learning_rate_max)}`}]),M("div",{className:"met-detail-section"},M("h3",null,"模型层"),$u(ul)),M("div",{className:"met-detail-section"},M("h3",null,"data/ 文件"),ju(s.files)),y.kind==="job"?M("div",{className:"met-detail-section"},M("h3",null,"日志尾部"),zu(If(y.data).logTail)):null)}return M("div",{className:"met-page","data-testid":"met-nonlinear-page"},M(t2,{title:"MET Nonlinear 训练编排",eyebrow:"D601 GPU 用户服务",loading:n.loading||n.actionBusy,actions:M("div",{className:"panel-actions"},M("button",{type:"button",className:"ghost-btn",onClick:j,disabled:n.loading,"data-testid":"met-refresh-button"},n.loading?"刷新中":"刷新"),M(v1,{title:"MET Nonlinear 用户服务",data:r,onOpen:l,testId:"raw-met-service"}))},M("div",{className:"findjob-hero"},M("div",null,M("div",{className:"node-version-line"},M(kt,{status:z.providerStatus==="online"?"online":"warn"},z.providerStatus||"unknown"),M("span",null,r.providerId),M("span",null,H.public?"公网暴露":"仅 UniDesk frontend 代理访问")),M("p",{className:"muted paragraph"},r.description)),M("div",{className:"microservice-ref-card"},M("span",null,"Repo"),M("strong",null,Z.url||"--"),M("code",null,Z.commitId||"--")),M("div",{className:"microservice-ref-card"},M("span",null,"D601 Docker"),M("strong",null,`${H.nodeBindHost||"--"}:${H.nodePort||"--"}`),M("code",null,`${Z.composeFile||"--"} / ${Z.containerName||"--"}`))),M(il,{error:n.error,wide:!0}),_.actionMessage?M("div",{className:"met-action-log","data-testid":"met-action-message"},_.actionMessage):null),M("div",{className:"met-grid"},M(t2,{title:"核心状态",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Queue + GPU",loading:n.loading},M("div",{className:"metric-grid"},M(Hn,{label:"Staged",value:E.staged??0,hint:"加入队列未开始",tone:Number(E.staged||0)>0?"warn":""}),M(Hn,{label:"Queued",value:E.queued??0,hint:"排队等待调度",tone:Number(E.queued||0)>0?"warn":""}),M(Hn,{label:"Running",value:E.running??0,hint:`max ${n.summary?.queue?.maxConcurrency??n.queue?.queue?.maxConcurrency??"--"}`,tone:Number(E.running||0)>0?"ok":""}),M(Hn,{label:"Succeeded",value:E.succeeded??0,hint:"已完成"}),M(Hn,{label:"Failed",value:E.failed??0,hint:"需要诊断",tone:Number(E.failed||0)>0?"warn":""}),M(Hn,{label:"2080Ti Free",value:h?vt(Number(h.freeRatio)*100):"--",hint:h?`${h.memoryFreeMiB}/${h.memoryTotalMiB} MiB`:"等待 GPU 上报"}),M(Hn,{label:"ML Image",value:V.present?"READY":"MISSING",hint:V.image||"met-nonlinear-ml:tf26",tone:V.present?"ok":"warn"}),M(Hn,{label:"Health",value:n.health?.ok?"OK":"--",hint:"D601 /health"}))),M(t2,{title:"队列控制",eyebrow:"Downloader-like staging",loading:n.actionBusy},M("div",{className:"met-control-strip"},M("label",null,"最大并发",M("input",{type:"number",min:1,max:16,value:_.maxConcurrency,"data-testid":"met-max-concurrency-input",onChange:(P)=>A({maxConcurrency:P.target.value})})),M("label",null,"目标 GPU",M("input",{value:_.targetGpuName,"data-testid":"met-target-gpu-input",onChange:(P)=>A({targetGpuName:P.target.value})})),M("button",{type:"button",className:"ghost-btn",onClick:J,disabled:n.actionBusy,"data-testid":"met-save-settings-button"},"保存设置"),M("button",{type:"button",className:"primary-btn",onClick:U,disabled:n.actionBusy||Number(E.staged||0)===0,"data-testid":"met-start-queue-button"},"启动队列")),M("p",{className:"muted paragraph"},"Project 先进入待启动队列,不会立即训练;点击启动队列后才切换为排队中,并由 D601 scheduler 按最大并发和 2080Ti 显存策略调度。")),M("section",{className:"panel met-workspace"},M("div",{className:"met-tabs",role:"tablist"},k.map((P)=>M("button",{key:P.id,type:"button",className:_.activeTab===P.id?"active":"",onClick:()=>A({activeTab:P.id}),"data-testid":`met-tab-${P.id}`},`${P.label} ${P.count}`))),M("div",{className:"panel-body"},_.activeTab==="projects"?M("div",{className:"met-form-grid","data-testid":"met-projects-pane"},M("div",{className:"met-fork-card"},M("h3",null,"Fork Project"),M("label",null,"源 Project",M("select",{value:m,"data-testid":"met-source-project-select",onChange:(P)=>A({sourceProject:P.target.value})},p.map((P)=>M("option",{key:P.projectPath,value:P.projectPath},`${P.projectPath} · ${P.useModel||"model?"}`)))),M("label",null,"Fork 数量",M("input",{type:"number",min:1,max:100,value:_.forkCount,"data-testid":"met-fork-count-input",onChange:(P)=>A({forkCount:P.target.value})})),M("label",null,"训练轮数",M("input",{type:"number",min:1,max:1e5,value:_.forkEpochs,"data-testid":"met-fork-epochs-input",onChange:(P)=>A({forkEpochs:P.target.value})})),M("label",null,"目标前缀",M("input",{value:_.forkPrefix,"data-testid":"met-fork-prefix-input",onChange:(P)=>A({forkPrefix:P.target.value})})),M("button",{type:"button",className:"primary-btn",onClick:L,disabled:n.actionBusy||!m,"data-testid":"met-fork-button"},"Fork Project"),M("p",{className:"muted paragraph"},"Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。")),M("div",{className:"met-project-list"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},`Existing Projects · ${(n.projects?.roots||[]).map((P)=>`${P.root} ${P.count}`).join(" / ")}`),M(nl,{title:"选择已有 Project",loading:n.loading||n.actionBusy})),M("button",{type:"button",className:"ghost-btn",onClick:w,disabled:n.actionBusy||Q().length===0,"data-testid":"met-stage-selected-button"},`加入待启动队列 (${Q().length})`)),p.length===0?M(p0,{title:"暂无 project",text:"等待 D601 返回 /api/projects"}):M("div",{className:"met-project-table","data-testid":"met-project-tree"},M("div",{className:"met-tree-header"},M("span",null,"文件树 Project"),M("span",null,"Model"),M("span",null,"Epochs"),M("span",null,"Progress"),M("span",null,"速度")),O.children.map((P)=>x(P)))),Wu()):null,_.activeTab==="current"?M("div",{"data-testid":"met-current-pane"},b(),I(X,"current"),Wu(),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET Queue",data:n.queue,onOpen:l,testId:"raw-met-queue"}))):null,_.activeTab==="completed"?M("div",{"data-testid":"met-completed-pane"},I(v.length>0?v:Y.filter((P)=>P.status==="succeeded"),"completed"),Wu()):null,_.activeTab==="failed"?M("div",{"data-testid":"met-failed-pane"},I(T.length>0?T:Y.filter((P)=>["failed","canceled"].includes(P.status)),"failed"),Wu(),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET History",data:n.history,onOpen:l,testId:"raw-met-history"}))):null,_.activeTab==="gpu"?M("div",{className:"met-gpu-pane","data-testid":"met-gpu-pane"},D.length===0?M(p0,{title:"暂无 GPU 上报",text:"等待 D601 met-nonlinear-ts 或 ML image 提供 nvidia-smi 数据"}):M("div",{className:"table-wrap"},M("table",null,M("thead",null,M("tr",null,M("th",null,"Index"),M("th",null,"Name"),M("th",null,"Free"),M("th",null,"Policy"))),M("tbody",null,D.map((P)=>M("tr",{key:P.index},M("td",null,P.index),M("td",null,P.name),M("td",null,`${P.memoryFreeMiB} / ${P.memoryTotalMiB} MiB`,M("div",{className:"met-progress"},M("span",{style:{width:vt(Number(P.freeRatio)*100)}}))),M("td",null,String(P.name||"").includes("2080")?"target 2080Ti, <20% 限制并发":"non-target")))))),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET Images",data:n.images,onOpen:l,testId:"raw-met-images"}))):null))))}var ic=[{id:"ops",label:"运行总览",code:"OPS",tabs:[{id:"status",label:"态势总览"},{id:"performance",label:"性能面板"},{id:"events",label:"事件摘要"},{id:"logs",label:"服务日志"}]},{id:"nodes",label:"资源节点",code:"NODE",tabs:[{id:"list",label:"节点清单"},{id:"monitor",label:"资源监控"},{id:"docker",label:"Docker 状态"},{id:"gateway",label:"网关版本"},{id:"labels",label:"资源标签"},{id:"heartbeats",label:"心跳状态"}]},{id:"tasks",label:"任务调度",code:"TASK",tabs:[{id:"dispatch",label:"下发任务"},{id:"scheduled",label:"定时任务"},{id:"pending",label:"待处理任务"},{id:"history",label:"任务历史"},{id:"results",label:"执行结果"}]},{id:"apps",label:"用户服务",code:"APP",routeSegment:"app",tabs:[{id:"catalog",label:"服务目录"},{id:"todo-note",label:"Todo Note"},{id:"findjob",label:"FindJob"},{id:"pipeline",label:"Pipeline"},{id:"met-nonlinear",label:"MET Nonlinear"},{id:"claudeqq",label:"ClaudeQQ"},{id:"baidu-netdisk",label:"Baidu Netdisk"},{id:"filebrowser",label:"File Browser"},{id:"oa-event-flow",label:"OA Event Flow"},{id:"v3sctl",label:"V3S Control"},{id:"code-queue",label:"Code Queue"},{id:"project-manager",label:"Project Manager"}]},{id:"config",label:"系统配置",code:"CFG",tabs:[{id:"topology",label:"连接拓扑"},{id:"auth",label:"认证策略"},{id:"security",label:"安全边界"}]}],st=Object.fromEntries(ic.map((u)=>[u.id,u.tabs[0]?.id??""]));function fO(u){let l=String(u||"").trim();if(!l)return"";try{return decodeURIComponent(l)}catch{return l}}function nc(u){let l=String(u||"/"),[f]=l.split(/[?#]/u,1);if(f==="/")return"/";let n=`/${f.split("/").map(fO).filter(Boolean).join("/")}`;return n.endsWith("/")?n:`${n}/`}function rO(u){let l=2166136261;for(let f of u)l^=f.charCodeAt(0),l=Math.imul(l,16777619);return Math.abs(l>>>0).toString(36)}function c2(u){return String(u||"").normalize("NFKD").replace(/[\u0300-\u036f]/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+|-+$/gu,"")}function sQ(u){return String(u||"").trim().toLowerCase().replace(/[\s/\\?#%]+/gu,"-").replace(/-+/gu,"-").replace(/^-+|-+$/gu,"")}function aQ(u){let l=c2(u.routeSegment||"")||sQ(u.routeSegment||"");if(l)return l;let f=c2(u.id||"");if(f)return f;let r=c2(u.label||"")||sQ(u.label||"");if(r)return r;return`route-${rO(JSON.stringify(u))}`}function A2(u,l){return`${u}:${l}`}function oQ(u){let l=u.map((_)=>{let c=aQ(_);return{..._,routeSegment:c,tabs:_.tabs.map((A)=>({...A,routeSegment:aQ(A)}))}}),f={},r={},n={},i=l.map((_)=>{let c=_.tabs[0]?.id??"";n[_.id]=c;let A=_.tabs.map((J)=>{let Q=`/${_.routeSegment}/${J.routeSegment}/`,w=[Q],L={moduleId:_.id,tabId:J.id};for(let U of w)f[nc(U)]=L;return r[A2(_.id,J.id)]=Q,{...J,canonicalPath:Q,aliases:w}}),j=`/${_.routeSegment}/`,F={moduleId:_.id,tabId:c};return f[nc(j)]=F,{..._,routeSegment:_.routeSegment,canonicalPath:j,tabs:A}}),y=i[0],t={moduleId:y?.id||"",tabId:y?.tabs[0]?.id||""};return f["/"]=t,{modules:i,moduleById:Object.fromEntries(i.map((_)=>[_.id,_])),defaultActiveTabs:n,routeMap:f,canonicalPathByTarget:r,fallbackTarget:t}}function j2(u,l){return u.routeMap[nc(l)]||u.fallbackTarget}function yc(u,l,f){return u.canonicalPathByTarget[A2(l,f)]||u.canonicalPathByTarget[A2(u.fallbackTarget.moduleId,u.fallbackTarget.tabId)]||"/"}function dQ(u,l){let f=u.routeMap[nc(l)];if(!f)return null;return yc(u,f.moduleId,f.tabId)}var cc=Pu(Jl(),1);var iu=cc.default.createElement,{useEffect:eQ,useMemo:nO}=cc.default,F2=cc.default.useState;function $c({status:u,children:l,title:f}){let r=String(u||"unknown").toLowerCase();return iu("span",{className:`status-badge ${r}`,title:f},l||u||"unknown")}function at({label:u,value:l,hint:f,tone:r}){return iu("article",{className:`metric-card ${r||""}`},iu("div",{className:"metric-label"},u),iu("div",{className:"metric-value"},l),iu("div",{className:"metric-hint"},f))}function tc({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return iu("section",{className:`panel ${n||""}`},iu("div",{className:"panel-head"},iu("div",null,l?iu("p",{className:"panel-eyebrow"},l):null,iu(nl,{title:u,loading:i})),f?iu("div",{className:"panel-actions"},f):null),iu("div",{className:"panel-body"},r))}function ot({title:u,data:l,onOpen:f,testId:r}){return iu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f?.(u,l)},"查看原始JSON")}function U2({title:u,text:l}){return iu("div",{className:"empty-state"},iu("strong",null,u),iu("span",null,l))}function iO(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:null}function m0(u){return Array.isArray(u)?u:[]}function gf(u){let l=Number(u);return Number.isFinite(l)?l.toLocaleString("zh-CN"):"--"}function uN(u,l=140){if(u===null||u===void 0)return"--";let f=typeof u==="string"?u:JSON.stringify(u),r=String(f||"").replace(/\s+/gu," ").trim();return r.length>l?`${r.slice(0,l-1)}...`:r||"--"}function yO(u){return m0(u?.tags).map((l)=>String(l||"").trim()).filter(Boolean)}function Zi(u){let l=Number(u);return Number.isFinite(l)&&l>=0?Math.floor(l):0}function tO(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function _O(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function $O(u){return String(u||"").split(/[\s,]+/u).map((l)=>l.trim()).filter(Boolean).join(",")}function _c(u,l){return`${u}/microservices/oa-event-flow/proxy${l}`}function cO(u){if(u.includes("error")||u.includes("failed"))return"failed";if(u.includes("stats"))return"ok";if(u.includes("step")||u.includes("updated"))return"running";return"queued"}function AO(u){let l=String(u?.subjectKind||"trace"),f=String(u?.subjectId||u?.scopeId||"");return f?`${l}:${f}`:String(u?.scopeId||"--")}function jO({tags:u}){let l=yO({tags:u}).slice(0,6);return iu("div",{className:"oa-tag-rail"},l.length===0?iu("span",{className:"muted"},"--"):l.map((f)=>iu("code",{key:f},f)))}function FO({events:u,onRaw:l}){let f=[...m0(u)].reverse();return f.length===0?iu(U2,{title:"事件表暂无记录",text:"等待 Code Queue 或 Pipeline 按 tag 发布 OA 事件"}):iu("div",{className:"table-wrap oa-event-table-wrap"},iu("table",{className:"oa-event-table","data-testid":"oa-event-flow-event-table"},iu("thead",null,iu("tr",null,iu("th",null,"Seq"),iu("th",null,"Type"),iu("th",null,"Source"),iu("th",null,"Aggregate"),iu("th",null,"Tags"),iu("th",null,"Payload"),iu("th",null,"Created"),iu("th",null,"Raw"))),iu("tbody",null,f.map((r)=>{let n=String(r?.type||"event"),i=`${String(r?.aggregateType||"--")}:${String(r?.aggregateId||"--")}`;return iu("tr",{key:r?.eventId||r?.sequence},iu("td",null,iu("code",null,gf(r?.sequence))),iu("td",null,iu($c,{status:cO(n)},n)),iu("td",null,iu("strong",null,r?.sourceId||"--"),iu("code",null,r?.sourceKind||"--")),iu("td",null,iu("code",null,i)),iu("td",null,iu(jO,{tags:r?.tags})),iu("td",null,iu("span",{className:"oa-payload-preview"},uN(r?.payload,180))),iu("td",null,qu(r?.createdAt)),iu("td",null,iu(ot,{title:`OA Event ${r?.sequence||""}`,data:r,onOpen:l,testId:`raw-oa-event-${r?.sequence||"unknown"}`})))}))))}function UO({stats:u,onRaw:l}){let f=m0(u);return f.length===0?iu(U2,{title:"统计中心暂无投影",text:"trace-stats-snapshot / trace-step-created 进入事件流后会更新这里"}):iu("div",{className:"table-wrap oa-stats-table-wrap"},iu("table",{className:"oa-stats-table","data-testid":"oa-event-flow-stats"},iu("thead",null,iu("tr",null,iu("th",null,"Scope"),iu("th",null,"Service"),iu("th",null,"STEP"),iu("th",null,"Read"),iu("th",null,"Edit"),iu("th",null,"Run"),iu("th",null,"Error"),iu("th",null,"Output Seq"),iu("th",null,"Revision"),iu("th",null,"Updated"),iu("th",null,"Raw"))),iu("tbody",null,f.map((r)=>iu("tr",{key:r?.scopeId||`${r?.serviceId}-${r?.subjectId}`},iu("td",null,iu("strong",null,AO(r)),iu("code",null,r?.scopeId||"--")),iu("td",null,iu($c,{status:String(r?.serviceId||"unknown")==="code-queue"?"running":"queued"},r?.serviceId||"--")),iu("td",null,iu("strong",null,gf(Zi(r?.stepCount??r?.llmStepCount)))),iu("td",null,gf(Zi(r?.readCount))),iu("td",null,gf(Zi(r?.editCount))),iu("td",null,gf(Zi(r?.runCount))),iu("td",null,gf(Zi(r?.errorCount))),iu("td",null,iu("code",null,gf(Zi(r?.outputMaxSeq)))),iu("td",null,gf(Zi(r?.statsRevision))),iu("td",null,qu(r?.updatedAt)),iu("td",null,iu(ot,{title:`OA Trace Stats ${r?.scopeId||""}`,data:r,onOpen:l,testId:`raw-oa-stats-${String(r?.scopeId||"unknown").replace(/[^a-zA-Z0-9_-]/gu,"_")}`})))))))}function lN({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((z)=>z.id==="oa-event-flow")||null,[n,i]=F2("service:code-queue"),[y,t]=F2({loading:!1,error:"",health:null,diagnostics:null,events:[],stats:[],refreshedAt:null}),[_,c]=F2({status:"idle",message:"未连接",lastEventAt:""}),A=nO(()=>$O(n),[n]);async function j(){if(!r)return;t((z)=>({...z,loading:!0,error:""}));try{let z=A?`tags=${encodeURIComponent(A)}&`:"",[Z,H,E,D]=await Promise.all([Tu(`${f}/microservices/oa-event-flow/health`,{failureFields:[]}),Tu(_c(f,"/api/diagnostics")),Tu(_c(f,`/api/events?${z}limit=100`)),Tu(_c(f,`/api/stats/trace?${z}limit=100`))]);t({loading:!1,error:"",health:Z,diagnostics:H,events:m0(E?.events),stats:m0(D?.stats),refreshedAt:new Date})}catch(z){t((Z)=>({...Z,loading:!1,error:Ou(z,"OA Event Flow 加载失败")}))}}if(eQ(()=>{j()},[r?.id,r?.runtime?.providerStatus,A]),eQ(()=>{if(!r||typeof EventSource>"u")return;let z=A?`?tags=${encodeURIComponent(A)}`:"",Z=new EventSource(`${_c(f,"/api/events/stream")}${z}`,{withCredentials:!0});c({status:"running",message:"SSE connecting",lastEventAt:""});let H=(h)=>{c({status:"online",message:uN(h.data,120),lastEventAt:new Date().toISOString()})},E=(h)=>{try{let V=JSON.parse(String(h.data||"{}"));c({status:"online",message:String(V?.type||h.type||"event"),lastEventAt:new Date().toISOString()}),t((S)=>{let p=[...m0(S.events).filter((m)=>String(m?.eventId||"")!==String(V?.eventId||"")),V].sort((m,X)=>Number(m?.sequence||0)-Number(X?.sequence||0)).slice(-100),O=V?.type==="trace-stats-updated"&&iO(V?.payload?.stats)?[V.payload.stats,...m0(S.stats).filter((m)=>String(m?.scopeId||"")!==String(V.payload.stats.scopeId||""))].slice(0,100):S.stats;return{...S,events:p,stats:O}})}catch(V){c({status:"warn",message:Ou(V,"SSE 事件解析失败"),lastEventAt:new Date().toISOString()})}},D=()=>{c((h)=>({...h,status:"warn",message:"SSE reconnecting"}))};return Z.addEventListener("hello",H),Z.addEventListener("task-updated",E),Z.addEventListener("queue-updated",E),Z.addEventListener("trace-step-created",E),Z.addEventListener("trace-stats-snapshot",E),Z.addEventListener("trace-stats-updated",E),Z.addEventListener("trace-error",E),Z.onerror=D,()=>Z.close()},[r?.id,f,A]),!r)return iu(U2,{title:"OA Event Flow 未登记",text:"请在 config.json 的 microservices 中登记 id=oa-event-flow"});let F=tO(r),J=_O(r),Q=y.diagnostics||{},w=y.health||{},L=Q.eventCount??w.eventCount,U=Q.traceStatsCount??w.traceStatsCount,N=Q.latestSequence??w.latestSequence,q=Q.pipelineBridge||w.pipelineBridge||{},W=m0(Q.eventTypes).slice(0,8);return iu("div",{className:"oa-event-flow-page","data-testid":"oa-event-flow-page"},iu(tc,{title:"OA Event Flow 控制台",eyebrow:"Unified OA Event Bus + Stats Projection",loading:y.loading,actions:iu("div",{className:"panel-actions"},iu("button",{type:"button",className:"ghost-btn",onClick:j,disabled:y.loading,"data-testid":"oa-event-flow-refresh"},y.loading?"刷新中":"刷新"),iu(ot,{title:"OA Event Flow Service",data:r,onOpen:l,testId:"raw-oa-event-flow-service"}))},iu("div",{className:"oa-flow-hero"},iu("div",null,iu("div",{className:"node-version-line"},iu($c,{status:w?.ok||F.providerStatus==="online"?"online":"warn"},w?.ok?"HEALTH OK":F.providerStatus||"unknown"),iu($c,{status:_.status},_.status.toUpperCase()),iu("span",null,J.public?"公网暴露":"仅 UniDesk frontend 代理访问")),iu("p",{className:"muted paragraph"},"独立事件流微服务统一承载 Code Queue 与 Pipeline 的事件发布、tag 订阅、事件表审计和 Trace/STEP 统计投影。")),iu("div",{className:"oa-flow-signal"},iu("span",null,"stream"),iu("strong",null,_.message||"--"),iu("code",null,_.lastEventAt?tl(new Date(_.lastEventAt)):"waiting"))),iu(il,{error:y.error,wide:!0})),iu("div",{className:"oa-flow-metrics"},iu(at,{label:"事件总量",value:gf(L),hint:`latest seq ${gf(N)}`,tone:"ok"}),iu(at,{label:"Trace Stats",value:gf(U),hint:"oa_trace_stats 投影"}),iu(at,{label:"SSE Clients",value:gf(w?.sseClientCount??m0(Q.sseClients).length),hint:_.message||"tag subscription"}),iu(at,{label:"Pipeline Bridge",value:q?.enabled?gf(q?.insertedCount):"OFF",hint:q?.lastError||q?.lastFinishedAt||`${q?.mode||"snapshot"} service:pipeline`}),iu(at,{label:"DB",value:w?.databaseReady||Q.databaseReady?"READY":"WAIT",hint:w?.databaseLastError||Q.databaseLastError||"PostgreSQL persisted"})),iu(tc,{title:"标签订阅",eyebrow:y.refreshedAt?`Updated ${tl(y.refreshedAt)}`:"Tag Pub/Sub"},iu("div",{className:"oa-filter-bar"},iu("label",null,iu("span",null,"tags"),iu("input",{value:n,onChange:(z)=>i(z.target.value),placeholder:"service:code-queue, trace","data-testid":"oa-event-flow-tag-filter"})),iu("div",{className:"oa-filter-presets"},iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("service:code-queue")},"Code Queue"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("service:pipeline")},"Pipeline"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("trace")},"Trace"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("")},"All")),iu("code",null,A||"all events")),iu("div",{className:"oa-type-strip"},W.length===0?iu("span",{className:"muted"},"等待事件类型统计"):W.map((z)=>iu("span",{key:z.type,className:"data-chip"},`${z.type} ${gf(z.count)}`)))),iu("div",{className:"oa-flow-grid"},iu(tc,{title:"事件表",eyebrow:"oa_events persisted log",className:"oa-flow-wide",loading:y.loading,actions:iu(ot,{title:"OA Event Query",data:{events:y.events,diagnostics:Q},onOpen:l,testId:"raw-oa-events"})},iu(FO,{events:y.events,onRaw:l})),iu(tc,{title:"统计中心",eyebrow:"oa_trace_stats read model",className:"oa-flow-wide",loading:y.loading,actions:iu(ot,{title:"OA Trace Stats",data:y.stats,onOpen:l,testId:"raw-oa-trace-stats"})},iu(UO,{stats:y.stats,onRaw:l}))))}var hn=Pu(Jl(),1);var ru=Pu(iN(),1),yu=Pu(Jl(),1);function Sl(u){if(typeof u==="string"||typeof u==="number")return""+u;let l="";if(Array.isArray(u)){for(let f=0,r;f{}};function tN(){for(var u=0,l=arguments.length,f={},r;u=0)r=f.slice(n+1),f=f.slice(0,n);if(f&&!l.hasOwnProperty(f))throw Error("unknown type: "+f);return{type:f,name:r}})}jc.prototype=tN.prototype={constructor:jc,on:function(u,l){var f=this._,r=KO(u+"",f),n,i=-1,y=r.length;if(arguments.length<2){while(++i0)for(var f=Array(n),r=0,n,i;r=0&&(l=u.slice(0,f))!=="xmlns")u=u.slice(f+1);return J2.hasOwnProperty(l)?{space:J2[l],local:u}:u}function Q2(u){let l;while(l=u.sourceEvent)u=l;return u}function Gf(u,l){if(u=Q2(u),l===void 0)l=u.currentTarget;if(l){var f=l.ownerSVGElement||l;if(f.createSVGPoint){var r=f.createSVGPoint();return r.x=u.clientX,r.y=u.clientY,r=r.matrixTransform(l.getScreenCTM().inverse()),[r.x,r.y]}if(l.getBoundingClientRect){var n=l.getBoundingClientRect();return[u.clientX-n.left-l.clientLeft,u.clientY-n.top-l.clientTop]}}return[u.pageX,u.pageY]}function zO(){}function Bn(u){return u==null?zO:function(){return this.querySelector(u)}}function N2(u){if(typeof u!=="function")u=Bn(u);for(var l=this._groups,f=l.length,r=Array(f),n=0;n=q)q=N+1;while(!(z=L[q])&&++q=0;)if(y=r[n]){if(i&&y.compareDocumentPosition(i)^4)i.parentNode.insertBefore(y,i);i=y}return this}function B2(u){if(!u)u=mO;function l(j,F){return j&&F?u(j.__data__,F.__data__):!j-!F}for(var f=this._groups,r=f.length,n=Array(r),i=0;il?1:u>=l?0:NaN}function V2(){var u=arguments[0];return arguments[0]=this,u.apply(null,arguments),this}function D2(){return Array.from(this)}function X2(){for(var u=this._groups,l=0,f=u.length;l1?this.each((l==null?bO:typeof l==="function"?kO:vO)(u,l,f==null?"":f)):Vn(this.node(),u)}function Vn(u,l){return u.style.getPropertyValue(l)||u_(u).getComputedStyle(u,null).getPropertyValue(l)}function IO(u){return function(){delete this[u]}}function gO(u,l){return function(){this[u]=l}}function sO(u,l){return function(){var f=l.apply(this,arguments);if(f==null)delete this[u];else this[u]=f}}function C2(u,l){return arguments.length>1?this.each((l==null?IO:typeof l==="function"?sO:gO)(u,l)):this.node()[u]}function _N(u){return u.trim().split(/^|\s+/)}function M2(u){return u.classList||new $N(u)}function $N(u){this._node=u,this._names=_N(u.getAttribute("class")||"")}$N.prototype={add:function(u){var l=this._names.indexOf(u);if(l<0)this._names.push(u),this._node.setAttribute("class",this._names.join(" "))},remove:function(u){var l=this._names.indexOf(u);if(l>=0)this._names.splice(l,1),this._node.setAttribute("class",this._names.join(" "))},contains:function(u){return this._names.indexOf(u)>=0}};function cN(u,l){var f=M2(u),r=-1,n=l.length;while(++r=0)f=l.slice(r+1),l=l.slice(0,r);return{type:l,name:f}})}function JH(u){return function(){var l=this.__on;if(!l)return;for(var f=0,r=-1,n=l.length,i;f()=>u;function n_(u,{sourceEvent:l,subject:f,target:r,identifier:n,active:i,x:y,y:t,dx:_,dy:c,dispatch:A}){Object.defineProperties(this,{type:{value:u,enumerable:!0,configurable:!0},sourceEvent:{value:l,enumerable:!0,configurable:!0},subject:{value:f,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:n,enumerable:!0,configurable:!0},active:{value:i,enumerable:!0,configurable:!0},x:{value:y,enumerable:!0,configurable:!0},y:{value:t,enumerable:!0,configurable:!0},dx:{value:_,enumerable:!0,configurable:!0},dy:{value:c,enumerable:!0,configurable:!0},_:{value:A}})}n_.prototype.on=function(){var u=this._.on.apply(this._,arguments);return u===this._?this:u};function ZH(u){return!u.ctrlKey&&!u.button}function OH(){return this.parentNode}function HH(u,l){return l==null?{x:u.x,y:u.y}:l}function BH(){return navigator.maxTouchPoints||"ontouchstart"in this}function i_(){var u=ZH,l=OH,f=HH,r=BH,n={},i=Oi("start","drag","end"),y=0,t,_,c,A,j=0;function F(W){W.on("mousedown.drag",J).filter(r).on("touchstart.drag",L).on("touchmove.drag",U,UN).on("touchend.drag touchcancel.drag",N).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function J(W,z){if(A||!u.call(this,W,z))return;var Z=q(this,l.call(this,W,z),W,z,"mouse");if(!Z)return;sl(W.view).on("mousemove.drag",Q,Hi).on("mouseup.drag",w,Hi),g1(W.view),Jc(W),c=!1,t=W.clientX,_=W.clientY,Z("start",W)}function Q(W){if(C0(W),!c){var z=W.clientX-t,Z=W.clientY-_;c=z*z+Z*Z>j}n.mouse("drag",W)}function w(W){sl(W.view).on("mousemove.drag mouseup.drag",null),f_(W.view,c),C0(W),n.mouse("end",W)}function L(W,z){if(!u.call(this,W,z))return;var Z=W.changedTouches,H=l.call(this,W,z),E=Z.length,D,h;for(D=0;D>8&15|l>>4&240,l>>4&15|l&240,(l&15)<<4|l&15,1):f===8?Qc(l>>24&255,l>>16&255,l>>8&255,(l&255)/255):f===4?Qc(l>>12&15|l>>8&240,l>>8&15|l>>4&240,l>>4&15|l&240,((l&15)<<4|l&15)/255):null):(l=DH.exec(u))?new sf(l[1],l[2],l[3],1):(l=XH.exec(u))?new sf(l[1]*255/100,l[2]*255/100,l[3]*255/100,1):(l=SH.exec(u))?Qc(l[1],l[2],l[3],l[4]):(l=YH.exec(u))?Qc(l[1]*255/100,l[2]*255/100,l[3]*255/100,l[4]):(l=pH.exec(u))?LN(l[1],l[2]/100,l[3]/100,1):(l=mH.exec(u))?LN(l[1],l[2]/100,l[3]/100,l[4]):JN.hasOwnProperty(u)?qN(JN[u]):u==="transparent"?new sf(NaN,NaN,NaN,0):null}function qN(u){return new sf(u>>16&255,u>>8&255,u&255,1)}function Qc(u,l,f,r){if(r<=0)u=l=f=NaN;return new sf(u,l,f,r)}function MH(u){if(!(u instanceof $_))u=kr(u);if(!u)return new sf;return u=u.rgb(),new sf(u.r,u.g,u.b,u.opacity)}function a1(u,l,f,r){return arguments.length===1?MH(u):new sf(u,l,f,r==null?1:r)}function sf(u,l,f,r){this.r=+u,this.g=+l,this.b=+f,this.opacity=+r}y_(sf,a1,l5($_,{brighter(u){return u=u==null?qc:Math.pow(qc,u),new sf(this.r*u,this.g*u,this.b*u,this.opacity)},darker(u){return u=u==null?t_:Math.pow(t_,u),new sf(this.r*u,this.g*u,this.b*u,this.opacity)},rgb(){return this},clamp(){return new sf(Vi(this.r),Vi(this.g),Vi(this.b),Wc(this.opacity))},displayable(){return-0.5<=this.r&&this.r<255.5&&(-0.5<=this.g&&this.g<255.5)&&(-0.5<=this.b&&this.b<255.5)&&(0<=this.opacity&&this.opacity<=1)},hex:WN,formatHex:WN,formatHex8:RH,formatRgb:wN,toString:wN}));function WN(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}`}function RH(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}${Bi((isNaN(this.opacity)?1:this.opacity)*255)}`}function wN(){let u=Wc(this.opacity);return`${u===1?"rgb(":"rgba("}${Vi(this.r)}, ${Vi(this.g)}, ${Vi(this.b)}${u===1?")":`, ${u})`}`}function Wc(u){return isNaN(u)?1:Math.max(0,Math.min(1,u))}function Vi(u){return Math.max(0,Math.min(255,Math.round(u)||0))}function Bi(u){return u=Vi(u),(u<16?"0":"")+u.toString(16)}function LN(u,l,f,r){if(r<=0)u=l=f=NaN;else if(f<=0||f>=1)u=l=NaN;else if(l<=0)u=NaN;return new vr(u,l,f,r)}function GN(u){if(u instanceof vr)return new vr(u.h,u.s,u.l,u.opacity);if(!(u instanceof $_))u=kr(u);if(!u)return new vr;if(u instanceof vr)return u;u=u.rgb();var l=u.r/255,f=u.g/255,r=u.b/255,n=Math.min(l,f,r),i=Math.max(l,f,r),y=NaN,t=i-n,_=(i+n)/2;if(t){if(l===i)y=(f-r)/t+(f0&&_<1?0:y;return new vr(y,t,_,u.opacity)}function zN(u,l,f,r){return arguments.length===1?GN(u):new vr(u,l,f,r==null?1:r)}function vr(u,l,f,r){this.h=+u,this.s=+l,this.l=+f,this.opacity=+r}y_(vr,zN,l5($_,{brighter(u){return u=u==null?qc:Math.pow(qc,u),new vr(this.h,this.s,this.l*u,this.opacity)},darker(u){return u=u==null?t_:Math.pow(t_,u),new vr(this.h,this.s,this.l*u,this.opacity)},rgb(){var u=this.h%360+(this.h<0)*360,l=isNaN(u)||isNaN(this.s)?0:this.s,f=this.l,r=f+(f<0.5?f:1-f)*l,n=2*f-r;return new sf(f5(u>=240?u-240:u+120,n,r),f5(u,n,r),f5(u<120?u+240:u-120,n,r),this.opacity)},clamp(){return new vr(KN(this.h),Nc(this.s),Nc(this.l),Wc(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&(0<=this.l&&this.l<=1)&&(0<=this.opacity&&this.opacity<=1)},formatHsl(){let u=Wc(this.opacity);return`${u===1?"hsl(":"hsla("}${KN(this.h)}, ${Nc(this.s)*100}%, ${Nc(this.l)*100}%${u===1?")":`, ${u})`}`}}));function KN(u){return u=(u||0)%360,u<0?u+360:u}function Nc(u){return Math.max(0,Math.min(1,u||0))}function f5(u,l,f){return(u<60?l+(f-l)*u/60:u<180?f:u<240?l+(f-l)*(240-u)/60:l)*255}function r5(u,l,f,r,n){var i=u*u,y=i*u;return((1-3*u+3*i-y)*l+(4-6*i+3*y)*f+(1+3*u+3*i-3*y)*r+y*n)/6}function n5(u){var l=u.length-1;return function(f){var r=f<=0?f=0:f>=1?(f=1,l-1):Math.floor(f*l),n=u[r],i=u[r+1],y=r>0?u[r-1]:2*n-i,t=r()=>u;function hH(u,l){return function(f){return u+f*l}}function bH(u,l,f){return u=Math.pow(u,f),l=Math.pow(l,f)-u,f=1/f,function(r){return Math.pow(u+r*l,f)}}function TN(u){return(u=+u)===1?Lc:function(l,f){return f-l?bH(l,f,u):c_(isNaN(l)?f:l)}}function Lc(u,l){var f=l-u;return f?hH(u,f):c_(isNaN(u)?l:u)}var Di=function u(l){var f=TN(l);function r(n,i){var y=f((n=a1(n)).r,(i=a1(i)).r),t=f(n.g,i.g),_=f(n.b,i.b),c=Lc(n.opacity,i.opacity);return function(A){return n.r=y(A),n.g=t(A),n.b=_(A),n.opacity=c(A),n+""}}return r.gamma=u,r}(1);function EN(u){return function(l){var f=l.length,r=Array(f),n=Array(f),i=Array(f),y,t;for(y=0;yf)if(i=l.slice(f,i),t[y])t[y]+=i;else t[++y]=i;if((r=r[0])===(n=n[0]))if(t[y])t[y]+=n;else t[++y]=n;else t[++y]=null,_.push({i:y,x:zf(r,n)});f=$5.lastIndex}if(f180)A+=360;else if(A-c>180)c+=360;F.push({i:j.push(n(j)+"rotate(",null,r)-2,x:zf(c,A)})}else if(A)j.push(n(j)+"rotate("+A+r)}function t(c,A,j,F){if(c!==A)F.push({i:j.push(n(j)+"skewX(",null,r)-2,x:zf(c,A)});else if(A)j.push(n(j)+"skewX("+A+r)}function _(c,A,j,F,J,Q){if(c!==j||A!==F){var w=J.push(n(J)+"scale(",null,",",null,")");Q.push({i:w-4,x:zf(c,j)},{i:w-2,x:zf(A,F)})}else if(j!==1||F!==1)J.push(n(J)+"scale("+j+","+F+")")}return function(c,A){var j=[],F=[];return c=u(c),A=u(A),i(c.translateX,c.translateY,A.translateX,A.translateY,j,F),y(c.rotate,A.rotate,j,F),t(c.skewX,A.skewX,j,F),_(c.scaleX,c.scaleY,A.scaleX,A.scaleY,j,F),c=A=null,function(J){var Q=-1,w=F.length,L;while(++Q=0)u._call.call(void 0,l);u=u._next}--d1}function mN(){Si=(Ec=U_.now())+Zc,d1=j_=0;try{MN()}finally{d1=0,jB(),Si=0}}function AB(){var u=U_.now(),l=u-Ec;if(l>PN)Zc-=l,Ec=u}function jB(){var u,l=Tc,f,r=1/0;while(l)if(l._call){if(r>l._time)r=l._time;u=l,l=l._next}else f=l._next,l._next=null,l=u?u._next=f:Tc=f;F_=u,F5(r)}function F5(u){if(d1)return;if(j_)j_=clearTimeout(j_);var l=u-Si;if(l>24){if(u<1/0)j_=setTimeout(mN,u-U_.now()-Zc);if(A_)A_=clearInterval(A_)}else{if(!A_)Ec=U_.now(),A_=setInterval(AB,PN);d1=1,CN(mN)}}function N_(u,l,f){var r=new J_;return l=l==null?0:+l,r.restart((n)=>{r.stop(),u(n+l)},l,f),r}var UB=Oi("start","end","cancel","interrupt"),JB=[],hN=0,RN=1,Bc=2,Hc=3,xN=4,Vc=5,q_=6;function M0(u,l,f,r,n,i){var y=u.__transition;if(!y)u.__transition={};else if(f in y)return;QB(u,f,{name:l,index:r,group:n,on:UB,tween:JB,time:i.time,delay:i.delay,duration:i.duration,ease:i.ease,timer:null,state:hN})}function W_(u,l){var f=al(u,l);if(f.state>hN)throw Error("too late; already scheduled");return f}function Ff(u,l){var f=al(u,l);if(f.state>Hc)throw Error("too late; already running");return f}function al(u,l){var f=u.__transition;if(!f||!(f=f[l]))throw Error("transition not found");return f}function QB(u,l,f){var r=u.__transition,n;r[l]=f,f.timer=Oc(i,0,f.time);function i(c){if(f.state=RN,f.timer.restart(y,f.delay,f.time),f.delay<=c)y(c-f.delay)}function y(c){var A,j,F,J;if(f.state!==RN)return _();for(A in r){if(J=r[A],J.name!==f.name)continue;if(J.state===Hc)return N_(y);if(J.state===xN)J.state=q_,J.timer.stop(),J.on.call("interrupt",u,u.__data__,J.index,J.group),delete r[A];else if(+ABc&&r.state=0)l=l.slice(0,f);return!l||l==="start"})}function pB(u,l,f){var r,n,i=YB(l)?W_:Ff;return function(){var y=i(this,u),t=y.on;if(t!==r)(n=(r=t).copy()).on(l,f);y.on=n}}function z5(u,l){var f=this._id;return arguments.length<2?al(this.node(),f).on.on(u):this.each(pB(f,u,l))}function mB(u){return function(){var l=this.parentNode;for(var f in this.__transition)if(+f!==u)return;if(l)l.removeChild(this)}}function T5(){return this.on("end.remove",mB(this._id))}function E5(u){var l=this._name,f=this._id;if(typeof u!=="function")u=Bn(u);for(var r=this._groups,n=r.length,i=Array(n),y=0;y()=>u;function p5(u,{sourceEvent:l,target:f,transform:r,dispatch:n}){Object.defineProperties(this,{type:{value:u,enumerable:!0,configurable:!0},sourceEvent:{value:l,enumerable:!0,configurable:!0},target:{value:f,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:n}})}function Ir(u,l,f){this.k=u,this.x=l,this.y=f}Ir.prototype={constructor:Ir,scale:function(u){return u===1?this:new Ir(this.k*u,this.x,this.y)},translate:function(u,l){return u===0&l===0?this:new Ir(this.k,this.x+this.k*u,this.y+this.k*l)},apply:function(u){return[u[0]*this.k+this.x,u[1]*this.k+this.y]},applyX:function(u){return u*this.k+this.x},applyY:function(u){return u*this.k+this.y},invert:function(u){return[(u[0]-this.x)/this.k,(u[1]-this.y)/this.k]},invertX:function(u){return(u-this.x)/this.k},invertY:function(u){return(u-this.y)/this.k},rescaleX:function(u){return u.copy().domain(u.range().map(this.invertX,this).map(u.invert,u))},rescaleY:function(u){return u.copy().domain(u.range().map(this.invertY,this).map(u.invert,u))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Yi=new Ir(1,0,0);K_.prototype=Ir.prototype;function K_(u){while(!u.__zoom)if(!(u=u.parentNode))return Yi;return u.__zoom}function xc(u){u.stopImmediatePropagation()}function pi(u){u.preventDefault(),u.stopImmediatePropagation()}function eB(u){return(!u.ctrlKey||u.type==="wheel")&&!u.button}function uV(){var u=this;if(u instanceof SVGElement){if(u=u.ownerSVGElement||u,u.hasAttribute("viewBox"))return u=u.viewBox.baseVal,[[u.x,u.y],[u.x+u.width,u.y+u.height]];return[[0,0],[u.width.baseVal.value,u.height.baseVal.value]]}return[[0,0],[u.clientWidth,u.clientHeight]]}function kN(){return this.__zoom||Yi}function lV(u){return-u.deltaY*(u.deltaMode===1?0.05:u.deltaMode?1:0.002)*(u.ctrlKey?10:1)}function fV(){return navigator.maxTouchPoints||"ontouchstart"in this}function rV(u,l,f){var r=u.invertX(l[0][0])-f[0][0],n=u.invertX(l[1][0])-f[1][0],i=u.invertY(l[0][1])-f[0][1],y=u.invertY(l[1][1])-f[1][1];return u.translate(n>r?(r+n)/2:Math.min(0,r)||Math.max(0,n),y>i?(i+y)/2:Math.min(0,i)||Math.max(0,y))}function G_(){var u=eB,l=uV,f=rV,r=lV,n=fV,i=[0,1/0],y=[[-1/0,-1/0],[1/0,1/0]],t=250,_=Xi,c=Oi("start","zoom","end"),A,j,F,J=500,Q=150,w=0,L=10;function U(O){O.property("__zoom",kN).on("wheel.zoom",E,{passive:!1}).on("mousedown.zoom",D).on("dblclick.zoom",h).filter(n).on("touchstart.zoom",V).on("touchmove.zoom",S).on("touchend.zoom touchcancel.zoom",p).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}U.transform=function(O,m,X,v){var T=O.selection?O.selection():O;if(T.property("__zoom",kN),O!==T)z(O,m,X,v);else T.interrupt().each(function(){Z(this,arguments).event(v).start().zoom(null,typeof m==="function"?m.apply(this,arguments):m).end()})},U.scaleBy=function(O,m,X,v){U.scaleTo(O,function(){var T=this.__zoom.k,Y=typeof m==="function"?m.apply(this,arguments):m;return T*Y},X,v)},U.scaleTo=function(O,m,X,v){U.transform(O,function(){var T=l.apply(this,arguments),Y=this.__zoom,k=X==null?W(T):typeof X==="function"?X.apply(this,arguments):X,I=Y.invert(k),b=typeof m==="function"?m.apply(this,arguments):m;return f(q(N(Y,b),k,I),T,y)},X,v)},U.translateBy=function(O,m,X,v){U.transform(O,function(){return f(this.__zoom.translate(typeof m==="function"?m.apply(this,arguments):m,typeof X==="function"?X.apply(this,arguments):X),l.apply(this,arguments),y)},null,v)},U.translateTo=function(O,m,X,v,T){U.transform(O,function(){var Y=l.apply(this,arguments),k=this.__zoom,I=v==null?W(Y):typeof v==="function"?v.apply(this,arguments):v;return f(Yi.translate(I[0],I[1]).scale(k.k).translate(typeof m==="function"?-m.apply(this,arguments):-m,typeof X==="function"?-X.apply(this,arguments):-X),Y,y)},v,T)};function N(O,m){return m=Math.max(i[0],Math.min(i[1],m)),m===O.k?O:new Ir(m,O.x,O.y)}function q(O,m,X){var v=m[0]-X[0]*O.k,T=m[1]-X[1]*O.k;return v===O.x&&T===O.y?O:new Ir(O.k,v,T)}function W(O){return[(+O[0][0]+ +O[1][0])/2,(+O[0][1]+ +O[1][1])/2]}function z(O,m,X,v){O.on("start.zoom",function(){Z(this,arguments).event(v).start()}).on("interrupt.zoom end.zoom",function(){Z(this,arguments).event(v).end()}).tween("zoom",function(){var T=this,Y=arguments,k=Z(T,Y).event(v),I=l.apply(T,Y),b=X==null?W(I):typeof X==="function"?X.apply(T,Y):X,o=Math.max(I[1][0]-I[0][0],I[1][1]-I[0][1]),g=T.__zoom,x=typeof m==="function"?m.apply(T,Y):m,lu=_(g.invert(b).concat(o/g.k),x.invert(b).concat(o/x.k));return function(_u){if(_u===1)_u=x;else{var $u=lu(_u),ju=o/$u[2];_u=new Ir(ju,b[0]-$u[0]*ju,b[1]-$u[1]*ju)}k.zoom(null,_u)}})}function Z(O,m,X){return!X&&O.__zooming||new H(O,m)}function H(O,m){this.that=O,this.args=m,this.active=0,this.sourceEvent=null,this.extent=l.apply(O,m),this.taps=0}H.prototype={event:function(O){if(O)this.sourceEvent=O;return this},start:function(){if(++this.active===1)this.that.__zooming=this,this.emit("start");return this},zoom:function(O,m){if(this.mouse&&O!=="mouse")this.mouse[1]=m.invert(this.mouse[0]);if(this.touch0&&O!=="touch")this.touch0[1]=m.invert(this.touch0[0]);if(this.touch1&&O!=="touch")this.touch1[1]=m.invert(this.touch1[0]);return this.that.__zoom=m,this.emit("zoom"),this},end:function(){if(--this.active===0)delete this.that.__zooming,this.emit("end");return this},emit:function(O){var m=sl(this.that).datum();c.call(O,this.that,new p5(O,{sourceEvent:this.sourceEvent,target:U,type:O,transform:this.that.__zoom,dispatch:c}),m)}};function E(O,...m){if(!u.apply(this,arguments))return;var X=Z(this,m).event(O),v=this.__zoom,T=Math.max(i[0],Math.min(i[1],v.k*Math.pow(2,r.apply(this,arguments)))),Y=Gf(O);if(X.wheel){if(X.mouse[0][0]!==Y[0]||X.mouse[0][1]!==Y[1])X.mouse[1]=v.invert(X.mouse[0]=Y);clearTimeout(X.wheel)}else if(v.k===T)return;else X.mouse=[Y,v.invert(Y)],Dn(this),X.start();pi(O),X.wheel=setTimeout(k,Q),X.zoom("mouse",f(q(N(v,T),X.mouse[0],X.mouse[1]),X.extent,y));function k(){X.wheel=null,X.end()}}function D(O,...m){if(F||!u.apply(this,arguments))return;var X=O.currentTarget,v=Z(this,m,!0).event(O),T=sl(O.view).on("mousemove.zoom",b,!0).on("mouseup.zoom",o,!0),Y=Gf(O,X),k=O.clientX,I=O.clientY;g1(O.view),xc(O),v.mouse=[Y,this.__zoom.invert(Y)],Dn(this),v.start();function b(g){if(pi(g),!v.moved){var x=g.clientX-k,lu=g.clientY-I;v.moved=x*x+lu*lu>w}v.event(g).zoom("mouse",f(q(v.that.__zoom,v.mouse[0]=Gf(g,X),v.mouse[1]),v.extent,y))}function o(g){T.on("mousemove.zoom mouseup.zoom",null),f_(g.view,v.moved),pi(g),v.event(g).end()}}function h(O,...m){if(!u.apply(this,arguments))return;var X=this.__zoom,v=Gf(O.changedTouches?O.changedTouches[0]:O,this),T=X.invert(v),Y=X.k*(O.shiftKey?0.5:2),k=f(q(N(X,Y),v,T),l.apply(this,m),y);if(pi(O),t>0)sl(this).transition().duration(t).call(z,k,v,O);else sl(this).call(U.transform,k,v,O)}function V(O,...m){if(!u.apply(this,arguments))return;var X=O.touches,v=X.length,T=Z(this,m,O.changedTouches.length===v).event(O),Y,k,I,b;xc(O);for(k=0;k"[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",error002:()=>"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",error003:(u)=>`Node type "${u}" not found. Using fallback type "default".`,error004:()=>"The React Flow parent container needs a width and a height to render the graph.",error005:()=>"Only child nodes can use a parent extent.",error006:()=>"Can't create edge. An edge needs a source and a target.",error007:(u)=>`The old edge with id=${u} does not exist.`,error009:(u)=>`Marker type "${u}" doesn't exist.`,error008:(u,{id:l,sourceHandle:f,targetHandle:r})=>`Couldn't create edge for ${u} handle id: "${u==="source"?f:r}", edge id: ${l}.`,error010:()=>"Handle: No node id found. Make sure to only use a Handle inside a custom Node.",error011:(u)=>`Edge type "${u}" not found. Using fallback type "default".`,error012:(u)=>`Node with id "${u}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,error013:(u="react")=>`It seems that you haven't loaded the styles. Please import '@xyflow/${u}/dist/style.css' or base.css to make sure everything is working properly.`,error014:()=>"useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",error015:()=>"It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."},ny=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]],R5=["Enter"," ","Escape"],x5={"node.a11yDescription.default":"Press enter or space to select a node. Press delete to remove it and escape to cancel.","node.a11yDescription.keyboardDisabled":"Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.","node.a11yDescription.ariaLiveMessage":({direction:u,x:l,y:f})=>`Moved selected node ${u}. New position, x: ${l}, y: ${f}`,"edge.a11yDescription.default":"Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.","controls.ariaLabel":"Control Panel","controls.zoomIn.ariaLabel":"Zoom In","controls.zoomOut.ariaLabel":"Zoom Out","controls.fitView.ariaLabel":"Fit View","controls.interactive.ariaLabel":"Toggle Interactivity","minimap.ariaLabel":"Mini Map","handle.ariaLabel":"Handle"},Yn;(function(u){u.Strict="strict",u.Loose="loose"})(Yn||(Yn={}));var x0;(function(u){u.Free="free",u.Vertical="vertical",u.Horizontal="horizontal"})(x0||(x0={}));var mi;(function(u){u.Partial="partial",u.Full="full"})(mi||(mi={}));var h5={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null,pointer:null},c0;(function(u){u.Bezier="default",u.Straight="straight",u.Step="step",u.SmoothStep="smoothstep",u.SimpleBezier="simplebezier"})(c0||(c0={}));var pn;(function(u){u.Arrow="arrow",u.ArrowClosed="arrowclosed"})(pn||(pn={}));var Lu;(function(u){u.Left="left",u.Top="top",u.Right="right",u.Bottom="bottom"})(Lu||(Lu={}));var IN={[Lu.Left]:Lu.Right,[Lu.Right]:Lu.Left,[Lu.Top]:Lu.Bottom,[Lu.Bottom]:Lu.Top};function b5(u){return u===null?null:u?"valid":"invalid"}var v5=(u)=>("id"in u)&&("source"in u)&&("target"in u),yq=(u)=>("id"in u)&&("position"in u)&&!("source"in u)&&!("target"in u),k5=(u)=>("id"in u)&&("internals"in u)&&!("source"in u)&&!("target"in u);var E_=(u,l=[0,0])=>{let{width:f,height:r}=A0(u),n=u.origin??l,i=f*n[0],y=r*n[1];return{x:u.position.x-i,y:u.position.y-y}},I5=(u,l={nodeOrigin:[0,0]})=>{if(u.length===0)return{x:0,y:0,width:0,height:0};let f=u.reduce((r,n)=>{let i=typeof n==="string",y=!l.nodeLookup&&!i?n:void 0;if(l.nodeLookup)y=i?l.nodeLookup.get(n):!k5(n)?l.nodeLookup.get(n.id):n;let t=y?vc(y,l.nodeOrigin):{x:0,y:0,x2:0,y2:0};return Ic(r,t)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return gc(f)},iy=(u,l={})=>{let f={x:1/0,y:1/0,x2:-1/0,y2:-1/0},r=!1;return u.forEach((n)=>{if(l.filter===void 0||l.filter(n))f=Ic(f,vc(n)),r=!0}),r?gc(f):{x:0,y:0,width:0,height:0}},kc=(u,l,[f,r,n]=[0,0,1],i=!1,y=!1)=>{let t={..._y(l,[f,r,n]),width:l.width/n,height:l.height/n},_=[];for(let c of u.values()){let{measured:A,selectable:j=!0,hidden:F=!1}=c;if(y&&!j||F)continue;let J=A.width??c.width??c.initialWidth??null,Q=A.height??c.height??c.initialHeight??null,w=yy(t,Ci(c)),L=(J??0)*(Q??0),U=i&&w>0;if(!c.internals.handleBounds||U||w>=L||c.dragging)_.push(c)}return _},tq=(u,l)=>{let f=new Set;return u.forEach((r)=>{f.add(r.id)}),l.filter((r)=>f.has(r.source)||f.has(r.target))};function nV(u,l){let f=new Map,r=l?.nodes?new Set(l.nodes.map((n)=>n.id)):null;return u.forEach((n)=>{if(n.measured.width&&n.measured.height&&(l?.includeHiddenNodes||!n.hidden)&&(!r||r.has(n.id)))f.set(n.id,n)}),f}async function _q({nodes:u,width:l,height:f,panZoom:r,minZoom:n,maxZoom:i},y){if(u.size===0)return Promise.resolve(!0);let t=nV(u,y),_=iy(t),c=Z_(_,l,f,y?.minZoom??n,y?.maxZoom??i,y?.padding??0.1);return await r.setViewport(c,{duration:y?.duration,ease:y?.ease,interpolate:y?.interpolate}),Promise.resolve(!0)}function g5({nodeId:u,nextPosition:l,nodeLookup:f,nodeOrigin:r=[0,0],nodeExtent:n,onError:i}){let y=f.get(u),t=y.parentId?f.get(y.parentId):void 0,{x:_,y:c}=t?t.internals.positionAbsolute:{x:0,y:0},A=y.origin??r,j=y.extent||n;if(y.extent==="parent"&&!y.expandParent)if(!t)i?.("005",Ur.error005());else{let J=t.measured.width,Q=t.measured.height;if(J&&Q)j=[[_,c],[_+J,c+Q]]}else if(t&&ry(y.extent))j=[[y.extent[0][0]+_,y.extent[0][1]+c],[y.extent[1][0]+_,y.extent[1][1]+c]];let F=ry(j)?Pi(l,j,y.measured):l;if(y.measured.width===void 0||y.measured.height===void 0)i?.("015",Ur.error015());return{position:{x:F.x-_+(y.measured.width??0)*A[0],y:F.y-c+(y.measured.height??0)*A[1]},positionAbsolute:F}}async function $q({nodesToRemove:u=[],edgesToRemove:l=[],nodes:f,edges:r,onBeforeDelete:n}){let i=new Set(u.map((F)=>F.id)),y=[];for(let F of f){if(F.deletable===!1)continue;let J=i.has(F.id),Q=!J&&F.parentId&&y.find((w)=>w.id===F.parentId);if(J||Q)y.push(F)}let t=new Set(l.map((F)=>F.id)),_=r.filter((F)=>F.deletable!==!1),A=tq(y,_);for(let F of _)if(t.has(F.id)&&!A.find((Q)=>Q.id===F.id))A.push(F);if(!n)return{edges:A,nodes:y};let j=await n({nodes:y,edges:A});if(typeof j==="boolean")return j?{edges:A,nodes:y}:{edges:[],nodes:[]};return j}var fy=(u,l=0,f=1)=>Math.min(Math.max(u,l),f),Pi=(u={x:0,y:0},l,f)=>({x:fy(u.x,l[0][0],l[1][0]-(f?.width??0)),y:fy(u.y,l[0][1],l[1][1]-(f?.height??0))});function cq(u,l,f){let{width:r,height:n}=A0(f),{x:i,y}=f.internals.positionAbsolute;return Pi(u,[[i,y],[i+r,y+n]],l)}var gN=(u,l,f)=>{if(uf)return-fy(Math.abs(u-f),1,l)/l;return 0},Aq=(u,l,f=15,r=40)=>{let n=gN(u.x,r,l.width-r)*f,i=gN(u.y,r,l.height-r)*f;return[n,i]},Ic=(u,l)=>({x:Math.min(u.x,l.x),y:Math.min(u.y,l.y),x2:Math.max(u.x2,l.x2),y2:Math.max(u.y2,l.y2)}),M5=({x:u,y:l,width:f,height:r})=>({x:u,y:l,x2:u+f,y2:l+r}),gc=({x:u,y:l,x2:f,y2:r})=>({x:u,y:l,width:f-u,height:r-l}),Ci=(u,l=[0,0])=>{let{x:f,y:r}=k5(u)?u.internals.positionAbsolute:E_(u,l);return{x:f,y:r,width:u.measured?.width??u.width??u.initialWidth??0,height:u.measured?.height??u.height??u.initialHeight??0}},vc=(u,l=[0,0])=>{let{x:f,y:r}=k5(u)?u.internals.positionAbsolute:E_(u,l);return{x:f,y:r,x2:f+(u.measured?.width??u.width??u.initialWidth??0),y2:r+(u.measured?.height??u.height??u.initialHeight??0)}},s5=(u,l)=>gc(Ic(M5(u),M5(l))),yy=(u,l)=>{let f=Math.max(0,Math.min(u.x+u.width,l.x+l.width)-Math.max(u.x,l.x)),r=Math.max(0,Math.min(u.y+u.height,l.y+l.height)-Math.max(u.y,l.y));return Math.ceil(f*r)},a5=(u)=>Zr(u.width)&&Zr(u.height)&&Zr(u.x)&&Zr(u.y),Zr=(u)=>!isNaN(u)&&isFinite(u),o5=(u,l)=>{},ty=(u,l=[1,1])=>{return{x:l[0]*Math.round(u.x/l[0]),y:l[1]*Math.round(u.y/l[1])}},_y=({x:u,y:l},[f,r,n],i=!1,y=[1,1])=>{let t={x:(u-f)/n,y:(l-r)/n};return i?ty(t,y):t},T_=({x:u,y:l},[f,r,n])=>{return{x:u*n+f,y:l*n+r}};function uy(u,l){if(typeof u==="number")return Math.floor((l-l/(1+u))*0.5);if(typeof u==="string"&&u.endsWith("px")){let f=parseFloat(u);if(!Number.isNaN(f))return Math.floor(f)}if(typeof u==="string"&&u.endsWith("%")){let f=parseFloat(u);if(!Number.isNaN(f))return Math.floor(l*f*0.01)}return console.error(`[React Flow] The padding value "${u}" is invalid. Please provide a number or a string with a valid unit (px or %).`),0}function iV(u,l,f){if(typeof u==="string"||typeof u==="number"){let r=uy(u,f),n=uy(u,l);return{top:r,right:n,bottom:r,left:n,x:n*2,y:r*2}}if(typeof u==="object"){let r=uy(u.top??u.y??0,f),n=uy(u.bottom??u.y??0,f),i=uy(u.left??u.x??0,l),y=uy(u.right??u.x??0,l);return{top:r,right:y,bottom:n,left:i,x:i+y,y:r+n}}return{top:0,right:0,bottom:0,left:0,x:0,y:0}}function yV(u,l,f,r,n,i){let{x:y,y:t}=T_(u,[l,f,r]),{x:_,y:c}=T_({x:u.x+u.width,y:u.y+u.height},[l,f,r]),A=n-_,j=i-c;return{left:Math.floor(y),top:Math.floor(t),right:Math.floor(A),bottom:Math.floor(j)}}var Z_=(u,l,f,r,n,i)=>{let y=iV(i,l,f),t=(l-y.x)/u.width,_=(f-y.y)/u.height,c=Math.min(t,_),A=fy(c,r,n),j=u.x+u.width/2,F=u.y+u.height/2,J=l/2-j*A,Q=f/2-F*A,w=yV(u,J,Q,A,l,f),L={left:Math.min(w.left-y.left,0),top:Math.min(w.top-y.top,0),right:Math.min(w.right-y.right,0),bottom:Math.min(w.bottom-y.bottom,0)};return{x:J-L.left+L.right,y:Q-L.top+L.bottom,zoom:A}},$y=()=>typeof navigator<"u"&&navigator?.userAgent?.indexOf("Mac")>=0;function ry(u){return u!==void 0&&u!==null&&u!=="parent"}function A0(u){return{width:u.measured?.width??u.width??u.initialWidth??0,height:u.measured?.height??u.height??u.initialHeight??0}}function d5(u){return(u.measured?.width??u.width??u.initialWidth)!==void 0&&(u.measured?.height??u.height??u.initialHeight)!==void 0}function e5(u,l={width:0,height:0},f,r,n){let i={...u},y=r.get(f);if(y){let t=y.origin||n;i.x+=y.internals.positionAbsolute.x-(l.width??0)*t[0],i.y+=y.internals.positionAbsolute.y-(l.height??0)*t[1]}return i}function u9(u,l){if(u.size!==l.size)return!1;for(let f of u)if(!l.has(f))return!1;return!0}function jq(){let u,l;return{promise:new Promise((r,n)=>{u=r,l=n}),resolve:u,reject:l}}function Fq(u){return{...x5,...u||{}}}function z_(u,{snapGrid:l=[0,0],snapToGrid:f=!1,transform:r,containerBounds:n}){let{x:i,y}=Or(u),t=_y({x:i-(n?.left??0),y:y-(n?.top??0)},r),{x:_,y:c}=f?ty(t,l):t;return{xSnapped:_,ySnapped:c,...t}}var sc=(u)=>({width:u.offsetWidth,height:u.offsetHeight}),l9=(u)=>u?.getRootNode?.()||window?.document,tV=["INPUT","SELECT","TEXTAREA"];function f9(u){let l=u.composedPath?.()?.[0]||u.target;if(l?.nodeType!==1)return!1;return tV.includes(l.nodeName)||l.hasAttribute("contenteditable")||!!l.closest(".nokey")}var r9=(u)=>("clientX"in u),Or=(u,l)=>{let f=r9(u),r=f?u.clientX:u.touches?.[0].clientX,n=f?u.clientY:u.touches?.[0].clientY;return{x:r-(l?.left??0),y:n-(l?.top??0)}},sN=(u,l,f,r,n)=>{let i=l.querySelectorAll(`.${u}`);if(!i||!i.length)return null;return Array.from(i).map((y)=>{let t=y.getBoundingClientRect();return{id:y.getAttribute("data-handleid"),type:u,nodeId:n,position:y.getAttribute("data-handlepos"),x:(t.left-f.left)/r,y:(t.top-f.top)/r,...sc(y)}})};function ac({sourceX:u,sourceY:l,targetX:f,targetY:r,sourceControlX:n,sourceControlY:i,targetControlX:y,targetControlY:t}){let _=u*0.125+n*0.375+y*0.375+f*0.125,c=l*0.125+i*0.375+t*0.375+r*0.125,A=Math.abs(_-u),j=Math.abs(c-l);return[_,c,A,j]}function hc(u,l){if(u>=0)return 0.5*u;return l*25*Math.sqrt(-u)}function aN({pos:u,x1:l,y1:f,x2:r,y2:n,c:i}){switch(u){case Lu.Left:return[l-hc(l-r,i),f];case Lu.Right:return[l+hc(r-l,i),f];case Lu.Top:return[l,f-hc(f-n,i)];case Lu.Bottom:return[l,f+hc(n-f,i)]}}function oc({sourceX:u,sourceY:l,sourcePosition:f=Lu.Bottom,targetX:r,targetY:n,targetPosition:i=Lu.Top,curvature:y=0.25}){let[t,_]=aN({pos:f,x1:u,y1:l,x2:r,y2:n,c:y}),[c,A]=aN({pos:i,x1:r,y1:n,x2:u,y2:l,c:y}),[j,F,J,Q]=ac({sourceX:u,sourceY:l,targetX:r,targetY:n,sourceControlX:t,sourceControlY:_,targetControlX:c,targetControlY:A});return[`M${u},${l} C${t},${_} ${c},${A} ${r},${n}`,j,F,J,Q]}function n9({sourceX:u,sourceY:l,targetX:f,targetY:r}){let n=Math.abs(f-u)/2,i=f0}var _V=({source:u,sourceHandle:l,target:f,targetHandle:r})=>`xy-edge__${u}${l||""}-${f}${r||""}`,$V=(u,l)=>{return l.some((f)=>f.source===u.source&&f.target===u.target&&(f.sourceHandle===u.sourceHandle||!f.sourceHandle&&!u.sourceHandle)&&(f.targetHandle===u.targetHandle||!f.targetHandle&&!u.targetHandle))},i9=(u,l,f={})=>{if(!u.source||!u.target)return o5("006",Ur.error006()),l;let r=f.getEdgeId||_V,n;if(v5(u))n={...u};else n={...u,id:r(u)};if($V(n,l))return l;if(n.sourceHandle===null)delete n.sourceHandle;if(n.targetHandle===null)delete n.targetHandle;return l.concat(n)};function dc({sourceX:u,sourceY:l,targetX:f,targetY:r}){let[n,i,y,t]=n9({sourceX:u,sourceY:l,targetX:f,targetY:r});return[`M ${u},${l}L ${f},${r}`,n,i,y,t]}var oN={[Lu.Left]:{x:-1,y:0},[Lu.Right]:{x:1,y:0},[Lu.Top]:{x:0,y:-1},[Lu.Bottom]:{x:0,y:1}},cV=({source:u,sourcePosition:l=Lu.Bottom,target:f})=>{if(l===Lu.Left||l===Lu.Right)return u.xMath.sqrt(Math.pow(l.x-u.x,2)+Math.pow(l.y-u.y,2));function AV({source:u,sourcePosition:l=Lu.Bottom,target:f,targetPosition:r=Lu.Top,center:n,offset:i,stepPosition:y}){let t=oN[l],_=oN[r],c={x:u.x+t.x*i,y:u.y+t.y*i},A={x:f.x+_.x*i,y:f.y+_.y*i},j=cV({source:c,sourcePosition:l,target:A}),F=j.x!==0?"x":"y",J=j[F],Q=[],w,L,U={x:0,y:0},N={x:0,y:0},[,,q,W]=n9({sourceX:u.x,sourceY:u.y,targetX:f.x,targetY:f.y});if(t[F]*_[F]===-1){if(F==="x")w=n.x??c.x+(A.x-c.x)*y,L=n.y??(c.y+A.y)/2;else w=n.x??(c.x+A.x)/2,L=n.y??c.y+(A.y-c.y)*y;let E=[{x:w,y:c.y},{x:w,y:A.y}],D=[{x:c.x,y:L},{x:A.x,y:L}];if(t[F]===J)Q=F==="x"?E:D;else Q=F==="x"?D:E}else{let E=[{x:c.x,y:A.y}],D=[{x:A.x,y:c.y}];if(F==="x")Q=t.x===J?D:E;else Q=t.y===J?E:D;if(l===r){let O=Math.abs(u[F]-f[F]);if(O<=i){let m=Math.min(i-1,i-O);if(t[F]===J)U[F]=(c[F]>u[F]?-1:1)*m;else N[F]=(A[F]>f[F]?-1:1)*m}}if(l!==r){let O=F==="x"?"y":"x",m=t[F]===_[O],X=c[O]>A[O],v=c[O]=p)w=(h.x+V.x)/2,L=Q[0].y;else w=Q[0].x,L=(h.y+V.y)/2}let z={x:c.x+U.x,y:c.y+U.y},Z={x:A.x+N.x,y:A.y+N.y};return[[u,...z.x!==Q[0].x||z.y!==Q[0].y?[z]:[],...Q,...Z.x!==Q[Q.length-1].x||Z.y!==Q[Q.length-1].y?[Z]:[],f],w,L,q,W]}function jV(u,l,f,r){let n=Math.min(dN(u,l)/2,dN(l,f)/2,r),{x:i,y}=l;if(u.x===i&&i===f.x||u.y===y&&y===f.y)return`L${i} ${y}`;if(u.y===y){let c=u.xf.id===l))||null}function ec(u,l){if(!u)return"";if(typeof u==="string")return u;return`${l?`${l}__`:""}${Object.keys(u).sort().map((r)=>`${r}=${u[r]}`).join("&")}`}function Nq(u,{id:l,defaultColor:f,defaultMarkerStart:r,defaultMarkerEnd:n}){let i=new Set;return u.reduce((y,t)=>{return[t.markerStart||r,t.markerEnd||n].forEach((_)=>{if(_&&typeof _==="object"){let c=ec(_,l);if(!i.has(c))y.push({id:c,color:_.color||f,..._}),i.add(c)}}),y},[]).sort((y,t)=>y.id.localeCompare(t.id))}var qq=1000,FV=10,y9={nodeOrigin:[0,0],nodeExtent:ny,elevateNodesOnSelect:!0,zIndexMode:"basic",defaults:{}},UV={...y9,checkEquality:!0};function t9(u,l){let f={...u};for(let r in l)if(l[r]!==void 0)f[r]=l[r];return f}function Wq(u,l,f){let r=t9(y9,f);for(let n of u.values())if(n.parentId)$9(n,u,l,r);else{let i=E_(n,r.nodeOrigin),y=ry(n.extent)?n.extent:r.nodeExtent,t=Pi(i,y,A0(n));n.internals.positionAbsolute=t}}function JV(u,l){if(!u.handles)return!u.measured?void 0:l?.internals.handleBounds;let f=[],r=[];for(let n of u.handles){let i={id:n.id,width:n.width??1,height:n.height??1,nodeId:u.id,x:n.x,y:n.y,position:n.position,type:n.type};if(n.type==="source")f.push(i);else if(n.type==="target")r.push(i)}return{source:f,target:r}}function _9(u){return u==="manual"}function u6(u,l,f,r={}){let n=t9(UV,r),i={i:0},y=new Map(l),t=n?.elevateNodesOnSelect&&!_9(n.zIndexMode)?qq:0,_=u.length>0,c=!1;l.clear(),f.clear();for(let A of u){let j=y.get(A.id);if(n.checkEquality&&A===j?.internals.userNode)l.set(A.id,j);else{let F=E_(A,n.nodeOrigin),J=ry(A.extent)?A.extent:n.nodeExtent,Q=Pi(F,J,A0(A));j={...n.defaults,...A,measured:{width:A.measured?.width,height:A.measured?.height},internals:{positionAbsolute:Q,handleBounds:JV(A,j),z:wq(A,t,n.zIndexMode),userNode:A}},l.set(A.id,j)}if((j.measured===void 0||j.measured.width===void 0||j.measured.height===void 0)&&!j.hidden)_=!1;if(A.parentId)$9(j,l,f,r,i);c||=A.selected??!1}return{nodesInitialized:_,hasSelectedNodes:c}}function QV(u,l){if(!u.parentId)return;let f=l.get(u.parentId);if(f)f.set(u.id,u);else l.set(u.parentId,new Map([[u.id,u]]))}function $9(u,l,f,r,n){let{elevateNodesOnSelect:i,nodeOrigin:y,nodeExtent:t,zIndexMode:_}=t9(y9,r),c=u.parentId,A=l.get(c);if(!A){console.warn(`Parent node ${c} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}if(QV(u,f),n&&!A.parentId&&A.internals.rootParentIndex===void 0&&_==="auto")A.internals.rootParentIndex=++n.i,A.internals.z=A.internals.z+n.i*FV;if(n&&A.internals.rootParentIndex!==void 0)n.i=A.internals.rootParentIndex;let j=i&&!_9(_)?qq:0,{x:F,y:J,z:Q}=NV(u,A,y,t,j,_),{positionAbsolute:w}=u.internals,L=F!==w.x||J!==w.y;if(L||Q!==u.internals.z)l.set(u.id,{...u,internals:{...u.internals,positionAbsolute:L?{x:F,y:J}:w,z:Q}})}function wq(u,l,f){let r=Zr(u.zIndex)?u.zIndex:0;if(_9(f))return r;return r+(u.selected?l:0)}function NV(u,l,f,r,n,i){let{x:y,y:t}=l.internals.positionAbsolute,_=A0(u),c=E_(u,f),A=ry(u.extent)?Pi(c,u.extent,_):c,j=Pi({x:y+A.x,y:t+A.y},r,_);if(u.extent==="parent")j=cq(j,_,l);let F=wq(u,n,i),J=l.internals.z??0;return{x:j.x,y:j.y,z:J>=F?J+1:F}}function l6(u,l,f,r=[0,0]){let n=[],i=new Map;for(let y of u){let t=l.get(y.parentId);if(!t)continue;let _=i.get(y.parentId)?.expandedRect??Ci(t),c=s5(_,y.rect);i.set(y.parentId,{expandedRect:c,parent:t})}if(i.size>0)i.forEach(({expandedRect:y,parent:t},_)=>{let c=t.internals.positionAbsolute,A=A0(t),j=t.origin??r,F=y.x0||J>0||L||U)n.push({id:_,type:"position",position:{x:t.position.x-F+L,y:t.position.y-J+U}}),f.get(_)?.forEach((N)=>{if(!u.some((q)=>q.id===N.id))n.push({id:N.id,type:"position",position:{x:N.position.x+F,y:N.position.y+J}})});if(A.width0){let J=l6(F,l,f,n);c.push(...J)}return{changes:c,updatedInternals:_}}async function Kq({delta:u,panZoom:l,transform:f,translateExtent:r,width:n,height:i}){if(!l||!u.x&&!u.y)return Promise.resolve(!1);let y=await l.setViewportConstrained({x:f[0]+u.x,y:f[1]+u.y,zoom:f[2]},[[0,0],[n,i]],r),t=!!y&&(y.x!==f[0]||y.y!==f[1]||y.k!==f[2]);return Promise.resolve(t)}function fq(u,l,f,r,n,i){let y=n,t=r.get(y)||new Map;r.set(y,t.set(f,l)),y=`${n}-${u}`;let _=r.get(y)||new Map;if(r.set(y,_.set(f,l)),i){y=`${n}-${u}-${i}`;let c=r.get(y)||new Map;r.set(y,c.set(f,l))}}function c9(u,l,f){u.clear(),l.clear();for(let r of f){let{source:n,target:i,sourceHandle:y=null,targetHandle:t=null}=r,_={edgeId:r.id,source:n,target:i,sourceHandle:y,targetHandle:t},c=`${n}-${y}--${i}-${t}`,A=`${i}-${t}--${n}-${y}`;fq("source",_,A,u,n,y),fq("target",_,c,u,i,t),l.set(r.id,r)}}function Gq(u,l){if(!u.parentId)return!1;let f=l.get(u.parentId);if(!f)return!1;if(f.selected)return!0;return Gq(f,l)}function rq(u,l,f){let r=u;do{if(r?.matches?.(l))return!0;if(r===f)return!1;r=r?.parentElement}while(r);return!1}function qV(u,l,f,r){let n=new Map;for(let[i,y]of u)if((y.selected||y.id===r)&&(!y.parentId||!Gq(y,u))&&(y.draggable||l&&typeof y.draggable>"u")){let t=u.get(i);if(t)n.set(i,{id:i,position:t.position||{x:0,y:0},distance:{x:f.x-t.internals.positionAbsolute.x,y:f.y-t.internals.positionAbsolute.y},extent:t.extent,parentId:t.parentId,origin:t.origin,expandParent:t.expandParent,internals:{positionAbsolute:t.internals.positionAbsolute||{x:0,y:0}},measured:{width:t.measured.width??0,height:t.measured.height??0}})}return n}function m5({nodeId:u,dragItems:l,nodeLookup:f,dragging:r=!0}){let n=[];for(let[y,t]of l){let _=f.get(y)?.internals.userNode;if(_)n.push({..._,position:t.position,dragging:r})}if(!u)return[n[0],n];let i=f.get(u)?.internals.userNode;return[!i?n[0]:{...i,position:l.get(u)?.position||i.position,dragging:r},n]}function WV({dragItems:u,snapGrid:l,x:f,y:r}){let n=u.values().next().value;if(!n)return null;let i={x:f-n.distance.x,y:r-n.distance.y},y=ty(i,l);return{x:y.x-i.x,y:y.y-i.y}}function zq({onNodeMouseDown:u,getStoreItems:l,onDragStart:f,onDrag:r,onDragStop:n}){let i={x:null,y:null},y=0,t=new Map,_=!1,c={x:0,y:0},A=null,j=!1,F=null,J=!1,Q=!1,w=null;function L({noDragClassName:N,handleSelector:q,domNode:W,isSelectable:z,nodeId:Z,nodeClickDistance:H=0}){F=sl(W);function E({x:S,y:p}){let{nodeLookup:O,nodeExtent:m,snapGrid:X,snapToGrid:v,nodeOrigin:T,onNodeDrag:Y,onSelectionDrag:k,onError:I,updateNodePositions:b}=l();i={x:S,y:p};let o=!1,g=t.size>1,x=g&&m?M5(iy(t)):null,lu=g&&v?WV({dragItems:t,snapGrid:X,x:S,y:p}):null;for(let[_u,$u]of t){if(!O.has(_u))continue;let ju={x:S-$u.distance.x,y:p-$u.distance.y};if(v)ju=lu?{x:Math.round(ju.x+lu.x),y:Math.round(ju.y+lu.y)}:ty(ju,X);let zu=null;if(g&&m&&!$u.extent&&x){let{positionAbsolute:e}=$u.internals,uu=e.x-x.x+m[0][0],Ku=e.x+$u.measured.width-x.x2+m[1][0],s=e.y-x.y+m[0][1],Nu=e.y+$u.measured.height-x.y2+m[1][1];zu=[[uu,s],[Ku,Nu]]}let{position:Wu,positionAbsolute:P}=g5({nodeId:_u,nextPosition:ju,nodeLookup:O,nodeExtent:zu?zu:m,nodeOrigin:T,onError:I});o=o||$u.position.x!==Wu.x||$u.position.y!==Wu.y,$u.position=Wu,$u.internals.positionAbsolute=P}if(Q=Q||o,!o)return;if(b(t,!0),w&&(r||Y||!Z&&k)){let[_u,$u]=m5({nodeId:Z,dragItems:t,nodeLookup:O});if(r?.(w,t,_u,$u),Y?.(w,_u,$u),!Z)k?.(w,$u)}}async function D(){if(!A)return;let{transform:S,panBy:p,autoPanSpeed:O,autoPanOnNodeDrag:m}=l();if(!m){_=!1,cancelAnimationFrame(y);return}let[X,v]=Aq(c,A,O);if(X!==0||v!==0){if(i.x=(i.x??0)-X/S[2],i.y=(i.y??0)-v/S[2],await p({x:X,y:v}))E(i)}y=requestAnimationFrame(D)}function h(S){let{nodeLookup:p,multiSelectionActive:O,nodesDraggable:m,transform:X,snapGrid:v,snapToGrid:T,selectNodesOnDrag:Y,onNodeDragStart:k,onSelectionDragStart:I,unselectNodesAndEdges:b}=l();if(j=!0,(!Y||!z)&&!O&&Z){if(!p.get(Z)?.selected)b()}if(z&&Y&&Z)u?.(Z);let o=z_(S.sourceEvent,{transform:X,snapGrid:v,snapToGrid:T,containerBounds:A});if(i=o,t=qV(p,m,o,Z),t.size>0&&(f||k||!Z&&I)){let[g,x]=m5({nodeId:Z,dragItems:t,nodeLookup:p});if(f?.(S.sourceEvent,t,g,x),k?.(S.sourceEvent,g,x),!Z)I?.(S.sourceEvent,x)}}let V=i_().clickDistance(H).on("start",(S)=>{let{domNode:p,nodeDragThreshold:O,transform:m,snapGrid:X,snapToGrid:v}=l();if(A=p?.getBoundingClientRect()||null,J=!1,Q=!1,w=S.sourceEvent,O===0)h(S);i=z_(S.sourceEvent,{transform:m,snapGrid:X,snapToGrid:v,containerBounds:A}),c=Or(S.sourceEvent,A)}).on("drag",(S)=>{let{autoPanOnNodeDrag:p,transform:O,snapGrid:m,snapToGrid:X,nodeDragThreshold:v,nodeLookup:T}=l(),Y=z_(S.sourceEvent,{transform:O,snapGrid:m,snapToGrid:X,containerBounds:A});if(w=S.sourceEvent,S.sourceEvent.type==="touchmove"&&S.sourceEvent.touches.length>1||Z&&!T.has(Z))J=!0;if(J)return;if(!_&&p&&j)_=!0,D();if(!j){let k=Or(S.sourceEvent,A),I=k.x-c.x,b=k.y-c.y;if(Math.sqrt(I*I+b*b)>v)h(S)}if((i.x!==Y.xSnapped||i.y!==Y.ySnapped)&&t&&j)c=Or(S.sourceEvent,A),E(Y)}).on("end",(S)=>{if(!j||J)return;if(_=!1,j=!1,cancelAnimationFrame(y),t.size>0){let{nodeLookup:p,updateNodePositions:O,onNodeDragStop:m,onSelectionDragStop:X}=l();if(Q)O(t,!1),Q=!1;if(n||m||!Z&&X){let[v,T]=m5({nodeId:Z,dragItems:t,nodeLookup:p,dragging:!1});if(n?.(S.sourceEvent,t,v,T),m?.(S.sourceEvent,v,T),!Z)X?.(S.sourceEvent,T)}}}).filter((S)=>{let p=S.target;return!S.button&&(!N||!rq(p,`.${N}`,W))&&(!q||rq(p,q,W))});F.call(V)}function U(){F?.on(".drag",null)}return{update:L,destroy:U}}function wV(u,l,f){let r=[],n={x:u.x-f,y:u.y-f,width:f*2,height:f*2};for(let i of l.values())if(yy(n,Ci(i))>0)r.push(i);return r}var LV=250;function KV(u,l,f,r){let n=[],i=1/0,y=wV(u,f,l+LV);for(let t of y){let _=[...t.internals.handleBounds?.source??[],...t.internals.handleBounds?.target??[]];for(let c of _){if(r.nodeId===c.nodeId&&r.type===c.type&&r.id===c.id)continue;let{x:A,y:j}=mn(t,c,c.position,!0),F=Math.sqrt(Math.pow(A-u.x,2)+Math.pow(j-u.y,2));if(F>l)continue;if(F1){let t=r.type==="source"?"target":"source";return n.find((_)=>_.type===t)??n[0]}return n[0]}function Tq(u,l,f,r,n,i=!1){let y=r.get(u);if(!y)return null;let t=n==="strict"?y.internals.handleBounds?.[l]:[...y.internals.handleBounds?.source??[],...y.internals.handleBounds?.target??[]],_=(f?t?.find((c)=>c.id===f):t?.[0])??null;return _&&i?{..._,...mn(y,_,_.position,!0)}:_}function Eq(u,l){if(u)return u;else if(l?.classList.contains("target"))return"target";else if(l?.classList.contains("source"))return"source";return null}function GV(u,l){let f=null;if(l)f=!0;else if(u&&!l)f=!1;return f}var Zq=()=>!0;function zV(u,{connectionMode:l,connectionRadius:f,handleId:r,nodeId:n,edgeUpdaterType:i,isTarget:y,domNode:t,nodeLookup:_,lib:c,autoPanOnConnect:A,flowId:j,panBy:F,cancelConnection:J,onConnectStart:Q,onConnect:w,onConnectEnd:L,isValidConnection:U=Zq,onReconnectEnd:N,updateConnection:q,getTransform:W,getFromHandle:z,autoPanSpeed:Z,dragThreshold:H=1,handleDomNode:E}){let D=l9(u.target),h=0,V,{x:S,y:p}=Or(u),O=Eq(i,E),m=t?.getBoundingClientRect(),X=!1;if(!m||!O)return;let v=Tq(n,O,r,_,l);if(!v)return;let T=Or(u,m),Y=!1,k=null,I=!1,b=null;function o(){if(!A||!m)return;let[Wu,P]=Aq(T,m,Z);F({x:Wu,y:P}),h=requestAnimationFrame(o)}let g={...v,nodeId:n,type:O,position:v.position},x=_.get(n),_u={inProgress:!0,isValid:null,from:mn(x,g,Lu.Left,!0),fromHandle:g,fromPosition:g.position,fromNode:x,to:T,toHandle:null,toPosition:IN[g.position],toNode:null,pointer:T};function $u(){X=!0,q(_u),Q?.(u,{nodeId:n,handleId:r,handleType:O})}if(H===0)$u();function ju(Wu){if(!X){let{x:Nu,y:Eu}=Or(Wu),Hu=Nu-S,vu=Eu-p;if(!(Hu*Hu+vu*vu>H*H))return;$u()}if(!z()||!g){zu(Wu);return}let P=W();if(T=Or(Wu,m),V=KV(_y(T,P,!1,[1,1]),f,_,g),!Y)o(),Y=!0;let e=Oq(Wu,{handle:V,connectionMode:l,fromNodeId:n,fromHandleId:r,fromType:y?"target":"source",isValidConnection:U,doc:D,lib:c,flowId:j,nodeLookup:_});b=e.handleDomNode,k=e.connection,I=GV(!!V,e.isValid);let uu=_.get(n),Ku=uu?mn(uu,g,Lu.Left,!0):_u.from,s={..._u,from:Ku,isValid:I,to:e.toHandle&&I?T_({x:e.toHandle.x,y:e.toHandle.y},P):T,toHandle:e.toHandle,toPosition:I&&e.toHandle?e.toHandle.position:IN[g.position],toNode:e.toHandle?_.get(e.toHandle.nodeId):null,pointer:T};q(s),_u=s}function zu(Wu){if("touches"in Wu&&Wu.touches.length>0)return;if(X){if((V||b)&&k&&I)w?.(k);let{inProgress:P,...e}=_u,uu={...e,toPosition:_u.toHandle?_u.toPosition:null};if(L?.(Wu,uu),i)N?.(Wu,uu)}J(),cancelAnimationFrame(h),Y=!1,I=!1,k=null,b=null,D.removeEventListener("mousemove",ju),D.removeEventListener("mouseup",zu),D.removeEventListener("touchmove",ju),D.removeEventListener("touchend",zu)}D.addEventListener("mousemove",ju),D.addEventListener("mouseup",zu),D.addEventListener("touchmove",ju),D.addEventListener("touchend",zu)}function Oq(u,{handle:l,connectionMode:f,fromNodeId:r,fromHandleId:n,fromType:i,doc:y,lib:t,flowId:_,isValidConnection:c=Zq,nodeLookup:A}){let j=i==="target",F=l?y.querySelector(`.${t}-flow__handle[data-id="${_}-${l?.nodeId}-${l?.id}-${l?.type}"]`):null,{x:J,y:Q}=Or(u),w=y.elementFromPoint(J,Q),L=w?.classList.contains(`${t}-flow__handle`)?w:F,U={handleDomNode:L,isValid:!1,connection:null,toHandle:null};if(L){let N=Eq(void 0,L),q=L.getAttribute("data-nodeid"),W=L.getAttribute("data-handleid"),z=L.classList.contains("connectable"),Z=L.classList.contains("connectableend");if(!q||!N)return U;let H={source:j?q:r,sourceHandle:j?W:n,target:j?r:q,targetHandle:j?n:W};U.connection=H;let D=z&&Z&&(f===Yn.Strict?j&&N==="source"||!j&&N==="target":q!==r||W!==n);U.isValid=D&&c(H),U.toHandle=Tq(q,N,W,A,f,!0)}return U}var f6={onPointerDown:zV,isValid:Oq};function Hq({domNode:u,panZoom:l,getTransform:f,getViewScale:r}){let n=sl(u);function i({translateExtent:t,width:_,height:c,zoomStep:A=1,pannable:j=!0,zoomable:F=!0,inversePan:J=!1}){let Q=(q)=>{if(q.sourceEvent.type!=="wheel"||!l)return;let W=f(),z=q.sourceEvent.ctrlKey&&$y()?10:1,Z=-q.sourceEvent.deltaY*(q.sourceEvent.deltaMode===1?0.05:q.sourceEvent.deltaMode?1:0.002)*A,H=W[2]*Math.pow(2,Z*z);l.scaleTo(H)},w=[0,0],L=(q)=>{if(q.sourceEvent.type==="mousedown"||q.sourceEvent.type==="touchstart")w=[q.sourceEvent.clientX??q.sourceEvent.touches[0].clientX,q.sourceEvent.clientY??q.sourceEvent.touches[0].clientY]},U=(q)=>{let W=f();if(q.sourceEvent.type!=="mousemove"&&q.sourceEvent.type!=="touchmove"||!l)return;let z=[q.sourceEvent.clientX??q.sourceEvent.touches[0].clientX,q.sourceEvent.clientY??q.sourceEvent.touches[0].clientY],Z=[z[0]-w[0],z[1]-w[1]];w=z;let H=r()*Math.max(W[2],Math.log(W[2]))*(J?-1:1),E={x:W[0]-Z[0]*H,y:W[1]-Z[1]*H},D=[[0,0],[_,c]];l.setViewportConstrained({x:E.x,y:E.y,zoom:W[2]},D,t)},N=G_().on("start",L).on("zoom",j?U:null).on("zoom.wheel",F?Q:null);n.call(N,{})}function y(){n.on("zoom",null)}return{update:i,destroy:y,pointer:Gf}}var r6=(u)=>({x:u.x,y:u.y,zoom:u.k}),P5=({x:u,y:l,zoom:f})=>Yi.translate(u,l).scale(f),ly=(u,l)=>u.target.closest(`.${l}`),Bq=(u,l)=>l===2&&Array.isArray(u)&&u.includes(2),TV=(u)=>((u*=2)<=1?u*u*u:(u-=2)*u*u+2)/2,C5=(u,l=0,f=TV,r=()=>{})=>{let n=typeof l==="number"&&l>0;if(!n)r();return n?u.transition().duration(l).ease(f).on("end",r):u},Vq=(u)=>{let l=u.ctrlKey&&$y()?10:1;return-u.deltaY*(u.deltaMode===1?0.05:u.deltaMode?1:0.002)*l};function EV({zoomPanValues:u,noWheelClassName:l,d3Selection:f,d3Zoom:r,panOnScrollMode:n,panOnScrollSpeed:i,zoomOnPinch:y,onPanZoomStart:t,onPanZoom:_,onPanZoomEnd:c}){return(A)=>{if(ly(A,l)){if(A.ctrlKey)A.preventDefault();return!1}A.preventDefault(),A.stopImmediatePropagation();let j=f.property("__zoom").k||1;if(A.ctrlKey&&y){let L=Gf(A),U=Vq(A),N=j*Math.pow(2,U);r.scaleTo(f,N,L,A);return}let F=A.deltaMode===1?20:1,J=n===x0.Vertical?0:A.deltaX*F,Q=n===x0.Horizontal?0:A.deltaY*F;if(!$y()&&A.shiftKey&&n!==x0.Vertical)J=A.deltaY*F,Q=0;r.translateBy(f,-(J/j)*i,-(Q/j)*i,{internal:!0});let w=r6(f.property("__zoom"));if(clearTimeout(u.panScrollTimeout),!u.isPanScrolling)u.isPanScrolling=!0,t?.(A,w);else _?.(A,w),u.panScrollTimeout=setTimeout(()=>{c?.(A,w),u.isPanScrolling=!1},150)}}function ZV({noWheelClassName:u,preventScrolling:l,d3ZoomHandler:f}){return function(r,n){let i=r.type==="wheel",y=!l&&i&&!r.ctrlKey,t=ly(r,u);if(r.ctrlKey&&i&&t)r.preventDefault();if(y||t)return null;r.preventDefault(),f.call(this,r,n)}}function OV({zoomPanValues:u,onDraggingChange:l,onPanZoomStart:f}){return(r)=>{if(r.sourceEvent?.internal)return;let n=r6(r.transform);if(u.mouseButton=r.sourceEvent?.button||0,u.isZoomingOrPanning=!0,u.prevViewport=n,r.sourceEvent?.type==="mousedown")l(!0);if(f)f?.(r.sourceEvent,n)}}function HV({zoomPanValues:u,panOnDrag:l,onPaneContextMenu:f,onTransformChange:r,onPanZoom:n}){return(i)=>{if(u.usedRightMouseButton=!!(f&&Bq(l,u.mouseButton??0)),!i.sourceEvent?.sync)r([i.transform.x,i.transform.y,i.transform.k]);if(n&&!i.sourceEvent?.internal)n?.(i.sourceEvent,r6(i.transform))}}function BV({zoomPanValues:u,panOnDrag:l,panOnScroll:f,onDraggingChange:r,onPanZoomEnd:n,onPaneContextMenu:i}){return(y)=>{if(y.sourceEvent?.internal)return;if(u.isZoomingOrPanning=!1,i&&Bq(l,u.mouseButton??0)&&!u.usedRightMouseButton&&y.sourceEvent)i(y.sourceEvent);if(u.usedRightMouseButton=!1,r(!1),n){let t=r6(y.transform);u.prevViewport=t,clearTimeout(u.timerId),u.timerId=setTimeout(()=>{n?.(y.sourceEvent,t)},f?150:0)}}}function VV({zoomActivationKeyPressed:u,zoomOnScroll:l,zoomOnPinch:f,panOnDrag:r,panOnScroll:n,zoomOnDoubleClick:i,userSelectionActive:y,noWheelClassName:t,noPanClassName:_,lib:c,connectionInProgress:A}){return(j)=>{let F=u||l,J=f&&j.ctrlKey,Q=j.type==="wheel";if(j.button===1&&j.type==="mousedown"&&(ly(j,`${c}-flow__node`)||ly(j,`${c}-flow__edge`)))return!0;if(!r&&!F&&!n&&!i&&!f)return!1;if(y)return!1;if(A&&!Q)return!1;if(ly(j,t)&&Q)return!1;if(ly(j,_)&&(!Q||n&&Q&&!u))return!1;if(!f&&j.ctrlKey&&Q)return!1;if(!f&&j.type==="touchstart"&&j.touches?.length>1)return j.preventDefault(),!1;if(!F&&!n&&!J&&Q)return!1;if(!r&&(j.type==="mousedown"||j.type==="touchstart"))return!1;if(Array.isArray(r)&&!r.includes(j.button)&&j.type==="mousedown")return!1;let w=Array.isArray(r)&&r.includes(j.button)||!j.button||j.button<=1;return(!j.ctrlKey||Q)&&w}}function Dq({domNode:u,minZoom:l,maxZoom:f,translateExtent:r,viewport:n,onPanZoom:i,onPanZoomStart:y,onPanZoomEnd:t,onDraggingChange:_}){let c={isZoomingOrPanning:!1,usedRightMouseButton:!1,prevViewport:{x:0,y:0,zoom:0},mouseButton:0,timerId:void 0,panScrollTimeout:void 0,isPanScrolling:!1},A=u.getBoundingClientRect(),j=G_().scaleExtent([l,f]).translateExtent(r),F=sl(u).call(j);N({x:n.x,y:n.y,zoom:fy(n.zoom,l,f)},[[0,0],[A.width,A.height]],r);let J=F.on("wheel.zoom"),Q=F.on("dblclick.zoom");j.wheelDelta(Vq);function w(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).transform(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function L({noWheelClassName:V,noPanClassName:S,onPaneContextMenu:p,userSelectionActive:O,panOnScroll:m,panOnDrag:X,panOnScrollMode:v,panOnScrollSpeed:T,preventScrolling:Y,zoomOnPinch:k,zoomOnScroll:I,zoomOnDoubleClick:b,zoomActivationKeyPressed:o,lib:g,onTransformChange:x,connectionInProgress:lu,paneClickDistance:_u,selectionOnDrag:$u}){if(O&&!c.isZoomingOrPanning)U();let ju=m&&!o&&!O;j.clickDistance($u?1/0:!Zr(_u)||_u<0?0:_u);let zu=ju?EV({zoomPanValues:c,noWheelClassName:V,d3Selection:F,d3Zoom:j,panOnScrollMode:v,panOnScrollSpeed:T,zoomOnPinch:k,onPanZoomStart:y,onPanZoom:i,onPanZoomEnd:t}):ZV({noWheelClassName:V,preventScrolling:Y,d3ZoomHandler:J});if(F.on("wheel.zoom",zu,{passive:!1}),!O){let P=OV({zoomPanValues:c,onDraggingChange:_,onPanZoomStart:y});j.on("start",P);let e=HV({zoomPanValues:c,panOnDrag:X,onPaneContextMenu:!!p,onPanZoom:i,onTransformChange:x});j.on("zoom",e);let uu=BV({zoomPanValues:c,panOnDrag:X,panOnScroll:m,onPaneContextMenu:p,onPanZoomEnd:t,onDraggingChange:_});j.on("end",uu)}let Wu=VV({zoomActivationKeyPressed:o,panOnDrag:X,zoomOnScroll:I,panOnScroll:m,zoomOnDoubleClick:b,zoomOnPinch:k,userSelectionActive:O,noPanClassName:S,noWheelClassName:V,lib:g,connectionInProgress:lu});if(j.filter(Wu),b)F.on("dblclick.zoom",Q);else F.on("dblclick.zoom",null)}function U(){j.on("zoom",null)}async function N(V,S,p){let O=P5(V),m=j?.constrain()(O,S,p);if(m)await w(m);return new Promise((X)=>X(m))}async function q(V,S){let p=P5(V);return await w(p,S),new Promise((O)=>O(p))}function W(V){if(F){let S=P5(V),p=F.property("__zoom");if(p.k!==V.zoom||p.x!==V.x||p.y!==V.y)j?.transform(F,S,null,{sync:!0})}}function z(){let V=F?K_(F.node()):{x:0,y:0,k:1};return{x:V.x,y:V.y,zoom:V.k}}function Z(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).scaleTo(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function H(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).scaleBy(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function E(V){j?.scaleExtent(V)}function D(V){j?.translateExtent(V)}function h(V){let S=!Zr(V)||V<0?0:V;j?.clickDistance(S)}return{update:L,destroy:U,setViewport:q,setViewportConstrained:N,getViewport:z,scaleTo:Z,scaleBy:H,setScaleExtent:E,setTranslateExtent:D,syncViewport:W,setClickDistance:h}}var Pn;(function(u){u.Line="line",u.Handle="handle"})(Pn||(Pn={}));function DV({width:u,prevWidth:l,height:f,prevHeight:r,affectsX:n,affectsY:i}){let y=u-l,t=f-r,_=[y>0?1:y<0?-1:0,t>0?1:t<0?-1:0];if(y&&n)_[0]=_[0]*-1;if(t&&i)_[1]=_[1]*-1;return _}function nq(u){let l=u.includes("right")||u.includes("left"),f=u.includes("bottom")||u.includes("top"),r=u.includes("left"),n=u.includes("top");return{isHorizontal:l,isVertical:f,affectsX:r,affectsY:n}}function Xn(u,l){return Math.max(0,l-u)}function Sn(u,l){return Math.max(0,u-l)}function bc(u,l,f){return Math.max(0,l-u,u-f)}function iq(u,l){return u?!l:l}function XV(u,l,f,r,n,i,y,t){let{affectsX:_,affectsY:c}=l,{isHorizontal:A,isVertical:j}=l,F=A&&j,{xSnapped:J,ySnapped:Q}=f,{minWidth:w,maxWidth:L,minHeight:U,maxHeight:N}=r,{x:q,y:W,width:z,height:Z,aspectRatio:H}=u,E=Math.floor(A?J-u.pointerX:0),D=Math.floor(j?Q-u.pointerY:0),h=z+(_?-E:E),V=Z+(c?-D:D),S=-i[0]*z,p=-i[1]*Z,O=bc(h,w,L),m=bc(V,U,N);if(y){let T=0,Y=0;if(_&&E<0)T=Xn(q+E+S,y[0][0]);else if(!_&&E>0)T=Sn(q+h+S,y[1][0]);if(c&&D<0)Y=Xn(W+D+p,y[0][1]);else if(!c&&D>0)Y=Sn(W+V+p,y[1][1]);O=Math.max(O,T),m=Math.max(m,Y)}if(t){let T=0,Y=0;if(_&&E>0)T=Sn(q+E,t[0][0]);else if(!_&&E<0)T=Xn(q+h,t[1][0]);if(c&&D>0)Y=Sn(W+D,t[0][1]);else if(!c&&D<0)Y=Xn(W+V,t[1][1]);O=Math.max(O,T),m=Math.max(m,Y)}if(n){if(A){let T=bc(h/H,U,N)*H;if(O=Math.max(O,T),y){let Y=0;if(!_&&!c||_&&!c&&F)Y=Sn(W+p+h/H,y[1][1])*H;else Y=Xn(W+p+(_?E:-E)/H,y[0][1])*H;O=Math.max(O,Y)}if(t){let Y=0;if(!_&&!c||_&&!c&&F)Y=Xn(W+h/H,t[1][1])*H;else Y=Sn(W+(_?E:-E)/H,t[0][1])*H;O=Math.max(O,Y)}}if(j){let T=bc(V*H,w,L)/H;if(m=Math.max(m,T),y){let Y=0;if(!_&&!c||c&&!_&&F)Y=Sn(q+V*H+S,y[1][0])/H;else Y=Xn(q+(c?D:-D)*H+S,y[0][0])/H;m=Math.max(m,Y)}if(t){let Y=0;if(!_&&!c||c&&!_&&F)Y=Xn(q+V*H,t[1][0])/H;else Y=Sn(q+(c?D:-D)*H,t[0][0])/H;m=Math.max(m,Y)}}}if(D=D+(D<0?m:-m),E=E+(E<0?O:-O),n)if(F)if(h>V*H)D=(iq(_,c)?-E:E)/H;else E=(iq(_,c)?-D:D)*H;else if(A)D=E/H,c=_;else E=D*H,_=c;let X=_?q+E:q,v=c?W+D:W;return{width:z+(_?-E:E),height:Z+(c?-D:D),x:i[0]*E*(!_?1:-1)+X,y:i[1]*D*(!c?1:-1)+v}}var Xq={width:0,height:0,x:0,y:0},SV={...Xq,pointerX:0,pointerY:0,aspectRatio:1};function YV(u){return[[0,0],[u.measured.width,u.measured.height]]}function pV(u,l,f){let r=l.position.x+u.position.x,n=l.position.y+u.position.y,i=u.measured.width??0,y=u.measured.height??0,t=f[0]*i,_=f[1]*y;return[[r-t,n-_],[r+i-t,n+y-_]]}function Sq({domNode:u,nodeId:l,getStoreItems:f,onChange:r,onEnd:n}){let i=sl(u),y={controlDirection:nq("bottom-right"),boundaries:{minWidth:0,minHeight:0,maxWidth:Number.MAX_VALUE,maxHeight:Number.MAX_VALUE},resizeDirection:void 0,keepAspectRatio:!1};function t({controlPosition:c,boundaries:A,keepAspectRatio:j,resizeDirection:F,onResizeStart:J,onResize:Q,onResizeEnd:w,shouldResize:L}){let U={...Xq},N={...SV};y={boundaries:A,resizeDirection:F,keepAspectRatio:j,controlDirection:nq(c)};let q=void 0,W=null,z=[],Z=void 0,H=void 0,E=void 0,D=!1,h=i_().on("start",(V)=>{let{nodeLookup:S,transform:p,snapGrid:O,snapToGrid:m,nodeOrigin:X,paneDomNode:v}=f();if(q=S.get(l),!q)return;W=v?.getBoundingClientRect()??null;let{xSnapped:T,ySnapped:Y}=z_(V.sourceEvent,{transform:p,snapGrid:O,snapToGrid:m,containerBounds:W});if(U={width:q.measured.width??0,height:q.measured.height??0,x:q.position.x??0,y:q.position.y??0},N={...U,pointerX:T,pointerY:Y,aspectRatio:U.width/U.height},Z=void 0,q.parentId&&(q.extent==="parent"||q.expandParent))Z=S.get(q.parentId),H=Z&&q.extent==="parent"?YV(Z):void 0;z=[],E=void 0;for(let[k,I]of S)if(I.parentId===l){if(z.push({id:k,position:{...I.position},extent:I.extent}),I.extent==="parent"||I.expandParent){let b=pV(I,q,I.origin??X);if(E)E=[[Math.min(b[0][0],E[0][0]),Math.min(b[0][1],E[0][1])],[Math.max(b[1][0],E[1][0]),Math.max(b[1][1],E[1][1])]];else E=b}}J?.(V,{...U})}).on("drag",(V)=>{let{transform:S,snapGrid:p,snapToGrid:O,nodeOrigin:m}=f(),X=z_(V.sourceEvent,{transform:S,snapGrid:p,snapToGrid:O,containerBounds:W}),v=[];if(!q)return;let{x:T,y:Y,width:k,height:I}=U,b={},o=q.origin??m,{width:g,height:x,x:lu,y:_u}=XV(N,y.controlDirection,X,y.boundaries,y.keepAspectRatio,o,H,E),$u=g!==k,ju=x!==I,zu=lu!==T&&$u,Wu=_u!==Y&&ju;if(!zu&&!Wu&&!$u&&!ju)return;if(zu||Wu||o[0]===1||o[1]===1){if(b.x=zu?lu:U.x,b.y=Wu?_u:U.y,U.x=b.x,U.y=b.y,z.length>0){let Ku=lu-T,s=_u-Y;for(let Nu of z)Nu.position={x:Nu.position.x-Ku+o[0]*(g-k),y:Nu.position.y-s+o[1]*(x-I)},v.push(Nu)}}if($u||ju)b.width=$u&&(!y.resizeDirection||y.resizeDirection==="horizontal")?g:U.width,b.height=ju&&(!y.resizeDirection||y.resizeDirection==="vertical")?x:U.height,U.width=b.width,U.height=b.height;if(Z&&q.expandParent){let Ku=o[0]*(b.width??0);if(b.x&&b.x{if(!D)return;w?.(V,{...U}),n?.({...U}),D=!1});i.call(h)}function _(){i.on(".drag",null)}return{update:t,destroy:_}}var kq=Pu(Jl(),1),Iq=Pu(xq(),1);var hq=(u)=>{let l,f=new Set,r=(A,j)=>{let F=typeof A==="function"?A(l):A;if(!Object.is(F,l)){let J=l;l=(j!=null?j:typeof F!=="object"||F===null)?F:Object.assign({},l,F),f.forEach((Q)=>Q(l,J))}},n=()=>l,_={setState:r,getState:n,getInitialState:()=>c,subscribe:(A)=>{return f.add(A),()=>f.delete(A)},destroy:()=>{f.clear()}},c=l=u(r,n,_);return _},bq=(u)=>u?hq(u):hq;var{useDebugValue:uD}=kq.default,{useSyncExternalStoreWithSelector:lD}=Iq.default,fD=(u)=>u;function j9(u,l=fD,f){let r=lD(u.subscribe,u.getState,u.getServerState||u.getInitialState,l,f);return uD(r),r}var vq=(u,l)=>{let f=bq(u),r=(n,i=l)=>j9(f,n,i);return Object.assign(r,f),r},gq=(u,l)=>u?vq(u,l):vq;function Gl(u,l){if(Object.is(u,l))return!0;if(typeof u!=="object"||u===null||typeof l!=="object"||l===null)return!1;if(u instanceof Map&&l instanceof Map){if(u.size!==l.size)return!1;for(let[r,n]of u)if(!Object.is(n,l.get(r)))return!1;return!0}if(u instanceof Set&&l instanceof Set){if(u.size!==l.size)return!1;for(let r of u)if(!l.has(r))return!1;return!0}let f=Object.keys(u);if(f.length!==Object.keys(l).length)return!1;for(let r of f)if(!Object.prototype.hasOwnProperty.call(l,r)||!Object.is(u[r],l[r]))return!1;return!0}var rD=Pu(WA(),1),_6=yu.createContext(null),nD=_6.Provider,qW=Ur.error001();function fl(u,l){let f=yu.useContext(_6);if(f===null)throw Error(qW);return j9(f,u,l)}function El(){let u=yu.useContext(_6);if(u===null)throw Error(qW);return yu.useMemo(()=>({getState:u.getState,setState:u.setState,subscribe:u.subscribe}),[u])}var sq={display:"none"},iD={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},WW="react-flow__node-desc",wW="react-flow__edge-desc",yD="react-flow__aria-live",tD=(u)=>u.ariaLiveMessage,_D=(u)=>u.ariaLabelConfig;function $D({rfId:u}){let l=fl(tD);return ru.jsx("div",{id:`${yD}-${u}`,"aria-live":"assertive","aria-atomic":"true",style:iD,children:l})}function cD({rfId:u,disableKeyboardA11y:l}){let f=fl(_D);return ru.jsxs(ru.Fragment,{children:[ru.jsx("div",{id:`${WW}-${u}`,style:sq,children:l?f["node.a11yDescription.default"]:f["node.a11yDescription.keyboardDisabled"]}),ru.jsx("div",{id:`${wW}-${u}`,style:sq,children:f["edge.a11yDescription.default"]}),!l&&ru.jsx($D,{rfId:u})]})}var $6=yu.forwardRef(({position:u="top-left",children:l,className:f,style:r,...n},i)=>{let y=`${u}`.split("-");return ru.jsx("div",{className:Sl(["react-flow__panel",f,...y]),style:r,ref:i,...n,children:l})});$6.displayName="Panel";function AD({proOptions:u,position:l="bottom-right"}){if(u?.hideAttribution)return null;return ru.jsx($6,{position:l,className:"react-flow__attribution","data-message":"Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev",children:ru.jsx("a",{href:"https://reactflow.dev",target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}var jD=(u)=>{let l=[],f=[];for(let[,r]of u.nodeLookup)if(r.selected)l.push(r.internals.userNode);for(let[,r]of u.edgeLookup)if(r.selected)f.push(r);return{selectedNodes:l,selectedEdges:f}},i6=(u)=>u.id;function FD(u,l){return Gl(u.selectedNodes.map(i6),l.selectedNodes.map(i6))&&Gl(u.selectedEdges.map(i6),l.selectedEdges.map(i6))}function UD({onSelectionChange:u}){let l=El(),{selectedNodes:f,selectedEdges:r}=fl(jD,FD);return yu.useEffect(()=>{let n={nodes:f,edges:r};u?.(n),l.getState().onSelectionChangeHandlers.forEach((i)=>i(n))},[f,r,u]),null}var JD=(u)=>!!u.onSelectionChangeHandlers;function QD({onSelectionChange:u}){let l=fl(JD);if(u||l)return ru.jsx(UD,{onSelectionChange:u});return null}var J9=typeof window<"u"?yu.useLayoutEffect:yu.useEffect,LW=[0,0],ND={x:0,y:0,zoom:1},qD=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraggable","autoPanOnNodeFocus","nodesConnectable","nodesFocusable","edgesFocusable","edgesReconnectable","elevateNodesOnSelect","elevateEdgesOnSelect","minZoom","maxZoom","nodeExtent","onNodesChange","onEdgesChange","elementsSelectable","connectionMode","snapGrid","snapToGrid","translateExtent","connectOnClick","defaultEdgeOptions","fitView","fitViewOptions","onNodesDelete","onEdgesDelete","onDelete","onNodeDrag","onNodeDragStart","onNodeDragStop","onSelectionDrag","onSelectionDragStart","onSelectionDragStop","onMoveStart","onMove","onMoveEnd","noPanClassName","nodeOrigin","autoPanOnConnect","autoPanOnNodeDrag","onError","connectionRadius","isValidConnection","selectNodesOnDrag","nodeDragThreshold","connectionDragThreshold","onBeforeDelete","debug","autoPanSpeed","ariaLabelConfig","zIndexMode"],aq=[...qD,"rfId"],WD=(u)=>({setNodes:u.setNodes,setEdges:u.setEdges,setMinZoom:u.setMinZoom,setMaxZoom:u.setMaxZoom,setTranslateExtent:u.setTranslateExtent,setNodeExtent:u.setNodeExtent,reset:u.reset,setDefaultNodesAndEdges:u.setDefaultNodesAndEdges}),oq={translateExtent:ny,nodeOrigin:LW,minZoom:0.5,maxZoom:2,elementsSelectable:!0,noPanClassName:"nopan",rfId:"1"};function wD(u){let{setNodes:l,setEdges:f,setMinZoom:r,setMaxZoom:n,setTranslateExtent:i,setNodeExtent:y,reset:t,setDefaultNodesAndEdges:_}=fl(WD,Gl),c=El();J9(()=>{return _(u.defaultNodes,u.defaultEdges),()=>{A.current=oq,t()}},[]);let A=yu.useRef(oq);return J9(()=>{for(let j of aq){let F=u[j],J=A.current[j];if(F===J)continue;if(typeof u[j]>"u")continue;if(j==="nodes")l(F);else if(j==="edges")f(F);else if(j==="minZoom")r(F);else if(j==="maxZoom")n(F);else if(j==="translateExtent")i(F);else if(j==="nodeExtent")y(F);else if(j==="ariaLabelConfig")c.setState({ariaLabelConfig:Fq(F)});else if(j==="fitView")c.setState({fitViewQueued:F});else if(j==="fitViewOptions")c.setState({fitViewOptions:F});else c.setState({[j]:F})}A.current=u},aq.map((j)=>u[j])),null}function dq(){if(typeof window>"u"||!window.matchMedia)return null;return window.matchMedia("(prefers-color-scheme: dark)")}function LD(u){let[l,f]=yu.useState(u==="system"?null:u);return yu.useEffect(()=>{if(u!=="system"){f(u);return}let r=dq(),n=()=>f(r?.matches?"dark":"light");return n(),r?.addEventListener("change",n),()=>{r?.removeEventListener("change",n)}},[u]),l!==null?l:dq()?.matches?"dark":"light"}var eq=typeof document<"u"?document:null;function H_(u=null,l={target:eq,actInsideInputWithModifier:!0}){let[f,r]=yu.useState(!1),n=yu.useRef(!1),i=yu.useRef(new Set([])),[y,t]=yu.useMemo(()=>{if(u!==null){let c=(Array.isArray(u)?u:[u]).filter((j)=>typeof j==="string").map((j)=>j.replace("+",` +html,body{width:${r}px!important;min-height:${n}px!important;overflow:hidden!important;}`,y.appendChild(t);let _=new XMLSerializer().serializeToString(i),c=`${_}`;YZ(new Blob([c],{type:"image/svg+xml;charset=utf-8"}),l.replace(/\.png$/i,".svg"))}function xQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=HZ(Array.isArray(u)?u:[]),n=new URLSearchParams(window.location.search).get("target")||"",i=n==="filebrowser-d518"?"filebrowser":n,y=r.some((Z)=>Z.id===i)?i:r[0]?.id||"",[t,_]=l2(y),[c,A]=l2({loading:!1,refreshedAt:null,health:{},error:""}),[j,F]=l2({exporting:!1,message:"",error:""}),J=EZ(null),Q=r.find((Z)=>Z.id===t)||r[0]||null,w=mQ(Q),L=PQ(Q),U=CQ(Q),N=Q?c.health[Q.id]:null,q=Q?VZ(f,Q.id,"/"):"about:blank";u2(()=>{if(r.length===0)return;if(!t||!r.some((Z)=>Z.id===t))_(r[0].id)},[r.map((Z)=>Z.id).join(",")]),u2(()=>{let Z=0,H=setInterval(()=>{if(Z+=1,n2(J.current)||Z>=24)clearInterval(H)},500);return()=>clearInterval(H)},[q]),u2(()=>{if(r.length===0)return;let Z=!1;async function H(){A((h)=>({...h,loading:!0,error:""}));let D=await Promise.all(r.map(async(h)=>{try{let V=await XZ(DZ(f,h.id));return[h.id,{ok:!0,body:V}]}catch(V){return[h.id,{ok:!1,error:Ou(V,"File Browser health failed")}]}}));if(Z)return;A({loading:!1,refreshedAt:new Date().toISOString(),health:Object.fromEntries(D),error:""})}H();let E=setInterval(H,30000);return()=>{Z=!0,clearInterval(E)}},[r.map((Z)=>`${Z.id}:${Z.runtime?.providerStatus||""}`).join(","),f]);function W(Z){_(Z);let H=new URL(window.location.href);H.searchParams.set("target",Z),window.history.replaceState({},"",`${H.pathname}${H.search}`)}async function z(){if(j.exporting)return;F({exporting:!0,message:"",error:""});try{let Z=new Date().toISOString().replace(/[-:.TZ]/g,"").slice(0,14);await pZ(J.current,`unidesk-filebrowser-${Q?.id||"target"}-${Z}.png`),F({exporting:!1,message:"截图已导出",error:""})}catch(Z){F({exporting:!1,message:"",error:Ou(Z,"截图导出失败")})}}if(r.length===0)return Vu(OZ,{title:"File Browser 未登记",text:"请在 config.json 的 microservices 中登记 id=filebrowser 或 filebrowser-* 用户服务"});return Vu("div",{className:"filebrowser-page","data-testid":"filebrowser-page"},c.error?Vu(il,{error:c.error,wide:!0}):null,Vu(f2,{title:"文件管理器",eyebrow:"File Browser / Host Files",loading:c.loading,actions:Vu("div",{className:"panel-actions"},Q?Vu("button",{type:"button",className:"ghost-btn",onClick:z,disabled:j.exporting,"data-testid":"filebrowser-export-screenshot"},j.exporting?"导出中...":"导出截图"):null,Q?Vu("a",{className:"ghost-btn",href:q,target:"_blank",rel:"noreferrer"},"新窗口打开"):null,Q?Vu(ZZ,{title:"File Browser 当前目标",data:{service:Q,health:N},onOpen:l,testId:"raw-filebrowser-active"}):null)},Vu("div",{className:"filebrowser-hero"},Vu("div",null,Vu("span",{className:`status-badge ${a3(N?.body)?"ok":"warn"}`},a3(N?.body)?"Health OK":"Health Pending"),Vu("h3",null,Q?.name||"File Browser"),Vu("p",{className:"muted paragraph"},Q?.description||"通过 UniDesk 登录态代理访问,不开放 File Browser 公网端口。"),j.error?Vu("p",{className:"filebrowser-shot-error"},j.error):null,j.message?Vu("p",{className:"filebrowser-shot-ok"},j.message):null),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Provider"),Vu("strong",null,Q?.providerId||"--"),Vu("code",null,w.providerName||Q?.providerId||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Private Backend"),Vu("strong",null,`${L.nodeBindHost||"--"}:${L.nodePort||"--"}`),Vu("code",null,L.nodeBaseUrl||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Image"),Vu("strong",null,U.dockerfile||"filebrowser/filebrowser:v2.63.3"),Vu("code",null,U.commitId||"--")),Vu("div",{className:"microservice-ref-card"},Vu("span",null,"Mount"),Vu("strong",null,MQ(Q)),Vu("code",null,Q?.providerId==="main-server"?"/root, /var, /home":"/home, /mnt/c, /mnt/d")))),Vu(f2,{title:"浏览目标",eyebrow:`${r.length} host targets`,loading:c.loading},Vu("div",{className:"filebrowser-target-grid"},r.map((Z)=>Vu(SZ,{key:Z.id,service:Z,active:Z.id===Q?.id,health:c.health[Z.id],onSelect:()=>W(Z.id),onRaw:l})))),Vu(f2,{title:`${BZ(Q)} 文件视图`,eyebrow:N?.body?`Health ${a3(N.body)?"OK":"UNKNOWN"} / ${c.refreshedAt?tl(c.refreshedAt):"--"}`:"Embedded WebUI",className:"filebrowser-frame-panel"},Vu("div",{className:"filebrowser-frame-shell"},Vu("div",{className:"filebrowser-frame-toolbar"},Vu("span",null,"BaseURL"),Vu("code",null,`/api/microservices/${Q?.id||"filebrowser"}/proxy`),Vu("span",null,"Root"),Vu("code",null,"/srv"),Vu("span",{className:"filebrowser-compact-note"},"Compact layout injected")),Vu("iframe",{ref:J,key:q,title:`${Q?.name||"File Browser"} WebUI`,src:q,className:"filebrowser-frame","data-testid":"filebrowser-frame",onLoad:(Z)=>n2(Z.currentTarget),sandbox:"allow-downloads allow-forms allow-modals allow-same-origin allow-scripts"}))))}var uc=Pu(Jl(),1);var Uu=uc.default.createElement,{useEffect:mZ}=uc.default,PZ=uc.default.useState;function o3({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return Uu("span",{className:`status-badge ${f}`},l||u||"unknown")}function On({label:u,value:l,hint:f,tone:r}){return Uu("article",{className:`metric-card ${r||""}`},Uu("div",{className:"metric-label"},u),Uu("div",{className:"metric-value"},l),Uu("div",{className:"metric-hint"},f))}function d3({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return Uu("section",{className:`panel ${n||""}`},Uu("div",{className:"panel-head"},Uu("div",null,l?Uu("p",{className:"panel-eyebrow"},l):null,Uu(nl,{title:u,loading:i})),f?Uu("div",{className:"panel-actions"},f):null),Uu("div",{className:"panel-body"},r))}function e3({title:u,data:l,onOpen:f,testId:r}){return Uu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function i2({title:u,text:l}){return Uu("div",{className:"empty-state"},Uu("strong",null,u),Uu("span",null,l))}function CZ(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function MZ(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function RZ(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function Ei(u,l){let f=u&&typeof u==="object"?u[l]:void 0;return Number.isFinite(Number(f))?String(f):"--"}function xZ(u){return(Array.isArray(u?.jobs)?u.jobs:[]).slice(0,40)}function hZ(u){return(Array.isArray(u?.drafts)?u.drafts:[]).slice(0,12)}function hQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((Q)=>Q.id==="findjob")||null,[n,i]=PZ({loading:!1,error:"",health:null,summary:null,jobs:null,drafts:null,refreshedAt:null});async function y(){if(!r)return;i((Q)=>({...Q,loading:!0,error:""}));try{let[Q,w,L,U]=await Promise.all([Tu(`${f}/microservices/findjob/health`),Tu(`${f}/microservices/findjob/proxy/api/summary`),Tu(`${f}/microservices/findjob/proxy/api/jobs?__unideskArrayLimit=jobs:40`),Tu(`${f}/microservices/findjob/proxy/api/drafts`)]);i({loading:!1,error:"",health:Q,summary:w,jobs:L,drafts:U,refreshedAt:new Date})}catch(Q){i((w)=>({...w,loading:!1,error:Ou(Q,"FindJob 加载失败")}))}}if(mZ(()=>{y()},[r?.id,r?.runtime?.providerStatus]),!r)return Uu(i2,{title:"FindJob 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=findjob"});let t=CZ(r),_=RZ(r),c=MZ(r),A=n.summary||{},j=xZ(n.jobs),F=hZ(n.drafts),J=n.jobs?._unidesk?.arrayLimits?.jobs;return Uu("div",{className:"findjob-page","data-testid":"findjob-page"},Uu(d3,{title:"FindJob 工作台",eyebrow:"D601 用户服务",loading:n.loading,actions:Uu("div",{className:"panel-actions"},Uu("button",{type:"button",className:"ghost-btn",onClick:y,disabled:n.loading,"data-testid":"findjob-refresh-button"},n.loading?"刷新中":"刷新"),Uu(e3,{title:"FindJob 用户服务",data:r,onOpen:l,testId:"raw-findjob-service"}))},Uu("div",{className:"findjob-hero"},Uu("div",null,Uu("div",{className:"node-version-line"},Uu(o3,{status:t.providerStatus==="online"?"online":"warn"},t.providerStatus||"unknown"),Uu("span",null,r.providerId),Uu("span",null,c.public?"公网暴露":"仅 UniDesk frontend 代理访问")),Uu("p",{className:"muted paragraph"},r.description)),Uu("div",{className:"microservice-ref-card"},Uu("span",null,"Repo"),Uu("strong",null,_.url||"--"),Uu("code",null,_.commitId||"--")),Uu("div",{className:"microservice-ref-card"},Uu("span",null,"D601 Docker"),Uu("strong",null,`${c.nodeBindHost||"--"}:${c.nodePort||"--"}`),Uu("code",null,`${_.composeFile||"--"} / ${_.composeService||"--"}`))),Uu(il,{error:n.error,wide:!0})),Uu("div",{className:"findjob-grid"},Uu(d3,{title:"岗位指标",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Summary",loading:n.loading},Uu("div",{className:"metric-grid"},Uu(On,{label:"岗位总量",value:Ei(A,"totalJobs"),hint:"tracked jobs",tone:"ok"}),Uu(On,{label:"原始岗位",value:Ei(A,"rawJobs"),hint:"raw queue"}),Uu(On,{label:"已验证",value:Ei(A,"verifiedJobs"),hint:"verified set"}),Uu(On,{label:"优先处理",value:Ei(A,"prioritizedJobs"),hint:"prioritized"}),Uu(On,{label:"过期",value:Ei(A,"staleJobs"),hint:"stale jobs",tone:"warn"}),Uu(On,{label:"无效",value:Ei(A,"invalidJobs"),hint:"invalid jobs",tone:"warn"}),Uu(On,{label:"上海",value:Ei(A,"shanghaiJobs"),hint:"city filter"}),Uu(On,{label:"Health",value:n.health?.ok?"OK":"--",hint:"D601 /api/health"})),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Summary",data:A,onOpen:l,testId:"raw-findjob-summary"}))),Uu(d3,{title:"近期岗位",eyebrow:J?`${J.returnedLength}/${J.originalLength} Preview`:`${j.length} Preview`,loading:n.loading},j.length===0?Uu(i2,{title:"暂无岗位预览",text:"等待 D601 findjob backend 返回 /api/jobs"}):Uu("div",{className:"table-wrap findjob-job-table"},Uu("table",null,Uu("thead",null,Uu("tr",null,Uu("th",null,"优先级"),Uu("th",null,"状态"),Uu("th",null,"单位"),Uu("th",null,"职位"),Uu("th",null,"城市"),Uu("th",null,"阶段"),Uu("th",null,"截止"),Uu("th",null,"证据"))),Uu("tbody",null,j.map((Q)=>Uu("tr",{key:Q.id},Uu("td",null,Uu(o3,{status:String(Q.priority||"").toLowerCase()||"unknown"},Q.priority||"--")),Uu("td",null,Uu(o3,{status:String(Q.status||"").toLowerCase()||"unknown"},Q.status||"--")),Uu("td",null,Q.organization_name||"--",Uu("code",null,Q.id||"--")),Uu("td",null,Q.display_title||Q.title||"--"),Uu("td",null,Q.display_city||Q.city||"--"),Uu("td",null,Q.workflow_stage||"--"),Uu("td",null,Q.deadline||"--"),Uu("td",null,Q.evidence_url?Uu("a",{href:Q.evidence_url,target:"_blank",rel:"noreferrer"},"打开"):Uu("span",{className:"muted"},"无"))))))),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Jobs Preview",data:n.jobs,onOpen:l,testId:"raw-findjob-jobs"}))),Uu(d3,{title:"草稿与报告",eyebrow:`${F.length} Drafts`,loading:n.loading},F.length===0?Uu(i2,{title:"暂无草稿",text:"D601 findjob backend 未返回 drafts"}):Uu("div",{className:"draft-list"},F.map((Q)=>Uu("article",{key:Q.id,className:"draft-card"},Uu("div",{className:"node-card-head"},Uu("strong",null,Q.id),Uu(o3,{status:Q.status},Q.status||"--")),Uu("div",{className:"docker-meta compact"},Uu("span",null,Q.workflow_stage||"--"),Uu("span",null,`jobs ${Q.counts?.jobs??0}`),Uu("span",null,`reports ${Q.counts?.reports??0}`)),Uu("span",null,Q.latestReportPath||"暂无报告"),Uu("code",null,qu(Q.updated_at||Q.updatedAt))))),Uu("div",{className:"panel-actions inline-actions"},Uu(e3,{title:"FindJob Drafts",data:n.drafts,onOpen:l,testId:"raw-findjob-drafts"})))))}var gt=Pu(Jl(),1);var M=gt.default.createElement,{useEffect:bZ}=gt.default,y2=gt.default.useState;function vt(u){let l=Number(u);return Number.isFinite(l)?`${Math.max(0,Math.min(100,l)).toFixed(1)}%`:"--"}function _2(u){if(u===null||u===void 0||u==="")return"--";let l=Number(u);if(!Number.isFinite(l))return"--";if(l<60)return`${Math.max(0,Math.round(l))}s`;if(l<3600)return`${Math.floor(l/60)}m ${Math.round(l%60)}s`;return`${Math.floor(l/3600)}h ${Math.floor(l%3600/60)}m`}function $2(u,l=2){let f=Number(u);if(!Number.isFinite(f))return u===!1?"false":u===!0?"true":"--";let r=Math.abs(f);if(Number.isInteger(f)||r>=1000)return f.toLocaleString("zh-CN",{maximumFractionDigits:0});if(r>=1)return f.toLocaleString("zh-CN",{maximumFractionDigits:l});return f.toLocaleString("zh-CN",{maximumFractionDigits:Math.max(l,6)})}function It(u){if(u===null||u===void 0||u==="")return"--";if(typeof u==="boolean")return u?"true":"false";if(typeof u==="number")return $2(u,4);if(Array.isArray(u))return u.map((l)=>It(l)).join(" x ");if(typeof u==="object")return"已上报";return String(u)}function lc(u){let l=Number(u);if(!Number.isFinite(l)||l<=0)return"--";let f=l>=100?0:l>=10?1:2;return`${l.toLocaleString("zh-CN",{maximumFractionDigits:f})} epoch/h`}function fc(u){return u.replace(/[^a-zA-Z0-9_-]/g,"-")}function If(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function kt({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return M("span",{className:`status-badge ${f}`},l||u||"unknown")}function Hn({label:u,value:l,hint:f,tone:r}){return M("article",{className:`metric-card ${r||""}`},M("div",{className:"metric-label"},u),M("div",{className:"metric-value"},l),M("div",{className:"metric-hint"},f))}function t2({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return M("section",{className:`panel ${n||""}`},M("div",{className:"panel-head"},M("div",null,l?M("p",{className:"panel-eyebrow"},l):null,M(nl,{title:u,loading:i})),f?M("div",{className:"panel-actions"},f):null),M("div",{className:"panel-body"},r))}function v1({title:u,data:l,onOpen:f,testId:r}){return M("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:(n)=>{n?.stopPropagation?.(),f(u,l)}},"查看原始JSON")}function p0({title:u,text:l}){return M("div",{className:"empty-state"},M("strong",null,u),M("span",null,l))}function vZ(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function kZ(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function IZ(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function gZ(u){return u?.counts&&typeof u.counts==="object"&&!Array.isArray(u.counts)?u.counts:{}}function sZ(u){return Array.isArray(u?.jobs)?u.jobs.slice(0,240):[]}function aZ(u){return Array.isArray(u?.projects)?u.projects.slice(0,1000):[]}function rc(u){return Array.isArray(u?.projects)?u.projects:[]}function oZ(u,l){if(Array.isArray(l?.gpu))return l.gpu;if(Array.isArray(u?.gpu))return u.gpu;return[]}function Fr(u,l){return`${u}/microservices/met-nonlinear/proxy${l}`}function bQ(u){return u.startedAt&&u.finishedAt?_2((Date.parse(u.finishedAt)-Date.parse(u.startedAt))/1000):"--"}function dZ(u){let l=u.progress||{};if(l.etaSeconds!==null&&l.etaSeconds!==void 0&&l.etaSeconds!==""){let y=Number(l.etaSeconds);if(Number.isFinite(y))return Math.max(0,y)}let f=Number(l.currentEpoch),r=Number(l.epochTarget??u.epochTarget),n=Date.parse(u.startedAt||"");if(!Number.isFinite(f)||f<=0||!Number.isFinite(r)||r<=f||!Number.isFinite(n))return null;let i=Math.max(0,(Date.now()-n)/1000);if(i<=0)return null;return Math.max(0,i/f*(r-f))}function vQ(u){let l=u.progress||{},f=Number(l.epochPerHour);if(Number.isFinite(f)&&f>0)return f;let r=Date.parse(u.startedAt||""),n=["succeeded","failed","canceled"].includes(u.status)?Date.parse(u.finishedAt||""):Date.now();if(!Number.isFinite(r)||!Number.isFinite(n)||n<=r)return null;let i=Number(l.currentEpoch??u.epochTarget);if(!Number.isFinite(i)||i<=0)return null;return i/((n-r)/3600000)}function kQ(u){if(u==="staged")return"待启动";if(u==="queued")return"排队中";if(u==="running")return"训练中";if(u==="succeeded")return"已完成";if(u==="failed")return"失败";if(u==="canceled")return"已取消";return u||"unknown"}function IQ(u,l,f){return{name:u,path:l,depth:f,count:0,children:[],project:null}}function eZ(u){let l=IQ("","",-1);for(let r of u){let i=String(r?.projectPath||"").replace(/\\/g,"/").split("/").filter(Boolean);if(i.length===0)continue;let y=l,t=[];for(let[_,c]of i.entries()){t.push(c);let A=t.join("/"),j=y.children.find((F)=>F.path===A);if(!j)j=IQ(c,A,_),y.children.push(j);if(_===i.length-1)j.project=r;y=j}}let f=(r)=>{let n=r.children.reduce((i,y)=>i+f(y),0);return r.count=(r.project?1:0)+n,r.children.sort((i,y)=>{if(Boolean(i.project)!==Boolean(y.project))return i.project?1:-1;return i.name.localeCompare(y.name,"zh-CN",{numeric:!0,sensitivity:"base"})}),r.count};return f(l),l}function uO(u){let l=If(u.data);return If(l.project).projectPath?If(l.project):l}function lO(u){return If(If(u.data).job)}function gQ({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((P)=>P.id==="met-nonlinear")||null,[n,i]=y2({loading:!1,actionBusy:!1,error:"",health:null,summary:null,queue:null,projects:null,history:null,images:null,refreshedAt:null}),[y,t]=y2({loading:!1,error:"",kind:"",key:"",title:"",data:null}),[_,c]=y2(()=>({activeTab:"projects",selectedProjects:{},expandedProjectDirs:{},sourceProject:"",forkCount:1,forkEpochs:200,forkPrefix:`ui_fork_${Date.now()}`,maxConcurrency:3,targetGpuName:"2080 Ti",actionMessage:""}));function A(P){c((e)=>({...e,...P}))}async function j(P=_.activeTab){if(!r)return;i((e)=>({...e,loading:!0,error:""}));try{let e=[["health",Tu(`${f}/microservices/met-nonlinear/health`)],["summary",Tu(Fr(f,"/api/summary"))]];if(P==="projects")e.push(["projectsRoot",Tu(Fr(f,"/api/projects?root=projects&limit=500"))]),e.push(["exProjectsRoot",Tu(Fr(f,"/api/projects?root=ex_projects&limit=500"))]);if(P==="current"||P==="completed"||P==="failed")e.push(["queue",Tu(Fr(f,"/api/queue"))]);if(P==="completed"||P==="failed")e.push(["history",Tu(Fr(f,"/api/history"))]);if(P==="gpu")e.push(["images",Tu(Fr(f,"/api/images"))]);let uu=Object.fromEntries(await Promise.all(e.map(async([s,Nu])=>[s,await Nu]))),Ku={loading:!1,actionBusy:!1,error:"",health:uu.health,summary:uu.summary,refreshedAt:new Date};if(uu.projectsRoot||uu.exProjectsRoot){let{projectsRoot:s,exProjectsRoot:Nu}=uu;Ku.projects={ok:s?.ok!==!1&&Nu?.ok!==!1,roots:[{root:"projects",count:rc(s).length},{root:"ex_projects",count:rc(Nu).length}],projects:[...rc(s),...rc(Nu)]}}if(uu.queue)Ku.queue=uu.queue;if(uu.history)Ku.history=uu.history;if(uu.images)Ku.images=uu.images;i((s)=>({...s,...Ku}))}catch(e){i((uu)=>({...uu,loading:!1,actionBusy:!1,error:Ou(e,"MET Nonlinear 加载失败")}))}}async function F(P,e){i((uu)=>({...uu,actionBusy:!0,error:""})),A({actionMessage:`${P}...`});try{let uu=await e();A({actionMessage:uu||`${P}完成`}),await j()}catch(uu){i((Ku)=>({...Ku,actionBusy:!1,error:Ou(uu,`${P}失败`)}))}}async function J(){await F("保存并发设置",async()=>{await Tu(Fr(f,"/api/queue/settings"),{method:"PUT",body:JSON.stringify({maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName})})})}function Q(){return Object.entries(_.selectedProjects).filter(([,P])=>P).map(([P])=>P)}async function w(){let P=Q();if(P.length===0)throw Error("请先选择至少一个 project");await F("加入待启动队列",async()=>{await Tu(Fr(f,"/api/queue"),{method:"POST",body:JSON.stringify({projectPaths:P,maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName,start:!1})}),A({activeTab:"current",selectedProjects:{}})})}async function L(){let P=_.sourceProject||p[0]?.projectPath;if(!P)throw Error("请先选择源 project");await F("Fork Project",async()=>{let e=await Tu(Fr(f,"/api/projects/fork"),{method:"POST",body:JSON.stringify({sourceProject:P,count:Number(_.forkCount),epochs:Number(_.forkEpochs),prefix:_.forkPrefix})}),uu=Array.isArray(e.projectPaths)?e.projectPaths:[],Ku=uu.reduce((s,Nu)=>{return s[Nu]=!0,s},{..._.selectedProjects});return A({selectedProjects:Ku}),`已 fork ${uu.length} 个 project,并已自动勾选;请确认后点击加入待启动队列。`})}async function U(){await F("启动队列",async()=>{await Tu(Fr(f,"/api/queue/start"),{method:"POST",body:JSON.stringify({maxConcurrency:Number(_.maxConcurrency),targetGpuName:_.targetGpuName})}),A({activeTab:"current"})})}async function N(P){await F("取消任务",async()=>{await Tu(Fr(f,`/api/jobs/${encodeURIComponent(P.id)}/cancel`),{method:"POST",body:JSON.stringify({})})})}async function q(P){let e=String(P?.projectPath||"");if(!e)return;t({loading:!0,error:"",kind:"project",key:e,title:e,data:null});try{let uu=await Tu(Fr(f,`/api/projects/config?path=${encodeURIComponent(e)}`));t({loading:!1,error:"",kind:"project",key:e,title:e,data:uu})}catch(uu){t({loading:!1,error:Ou(uu,"Project 详情加载失败"),kind:"project",key:e,title:e,data:null})}}async function W(P){let e=String(P?.id||"");if(!e)return;t({loading:!0,error:"",kind:"job",key:e,title:P.projectPath||e,data:null});try{let uu=await Tu(Fr(f,`/api/jobs/${encodeURIComponent(e)}`));t({loading:!1,error:"",kind:"job",key:e,title:uu?.job?.projectPath||P.projectPath||e,data:uu})}catch(uu){t({loading:!1,error:Ou(uu,"Job 详情加载失败"),kind:"job",key:e,title:P.projectPath||e,data:null})}}if(bZ(()=>{j(_.activeTab)},[r?.id,r?.runtime?.providerStatus,_.activeTab]),!r)return M(p0,{title:"MET Nonlinear 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=met-nonlinear"});let z=vZ(r),Z=IZ(r),H=kZ(r),E=gZ(n.queue?.queue||n.summary?.queue),D=oZ(n.health,n.queue),h=n.health?.targetGpu||n.summary?.targetGpu||D.find((P)=>String(P.name||"").includes("2080")),V=n.images?.mlImage||n.health?.image||{},S=sZ(n.queue),p=aZ(n.projects),O=eZ(p),m=_.sourceProject||p[0]?.projectPath||"",X=S.filter((P)=>["staged","queued","running"].includes(P.status)),v=S.filter((P)=>P.status==="succeeded"),T=S.filter((P)=>["failed","canceled"].includes(P.status)),Y=Array.isArray(n.history?.jobs)?n.history.jobs.slice(0,120):[],k=[{id:"projects",label:"项目库",count:p.length},{id:"current",label:"当前队列",count:X.length||Number(E.staged||0)+Number(E.queued||0)+Number(E.running||0)},{id:"completed",label:"已完成",count:v.length||Number(E.succeeded||0)},{id:"failed",label:"失败诊断",count:T.length||Number(E.failed||0)+Number(E.canceled||0)},{id:"gpu",label:"GPU/镜像",count:D.length}];function I(P,e){if(P.length===0)return M(p0,{title:e==="current"?"当前队列为空":"暂无记录",text:e==="current"?"从项目库选择或 fork project 后先加入待启动队列,再启动队列。":"终态任务会显示耗时、exit code 和失败诊断。"});return M("div",{className:"table-wrap met-job-table"},M("table",null,M("thead",null,M("tr",null,M("th",null,"状态"),M("th",null,"Project"),M("th",null,"Epoch"),M("th",null,"速度"),M("th",null,"ETA/耗时"),M("th",null,"GPU"),M("th",null,"Exit"),M("th",null,"更新时间"),M("th",null,"操作"))),M("tbody",null,P.map((uu)=>{let Ku=uu.progress||{},s=["staged","queued","running"].includes(uu.status),Nu=y.kind==="job"&&y.key===uu.id;return M("tr",{key:uu.id,className:`met-click-row ${Nu?"active":""}`,onClick:()=>W(uu),"data-testid":`met-job-row-${fc(uu.id)}`},M("td",null,M(kt,{status:uu.status},kQ(uu.status))),M("td",null,M("button",{type:"button",className:"met-inline-link",onClick:(Eu)=>{Eu.stopPropagation(),W(uu)}},uu.projectPath),M("code",null,uu.id)),M("td",null,M("span",null,`${Ku.currentEpoch??"--"} / ${Ku.epochTarget??uu.epochTarget??"--"}`),M("div",{className:"met-progress"},M("span",{style:{width:vt(Ku.progressPercent)}}))),M("td",null,M("strong",null,lc(vQ(uu)))),M("td",null,uu.status==="succeeded"||uu.status==="failed"||uu.status==="canceled"?bQ(uu):uu.status==="running"?`ETA ${_2(dZ(uu))}`:"--"),M("td",null,uu.gpuName||"--"),M("td",null,uu.exitCode??"--"),M("td",null,qu(uu.updatedAt)),M("td",null,s?M("button",{type:"button",className:"ghost-btn mini",onClick:(Eu)=>{Eu.stopPropagation(),N(uu)},disabled:n.actionBusy},"取消"):null,M(v1,{title:`MET Job ${uu.id}`,data:uu,onOpen:l,testId:`raw-met-job-${uu.id}`})))}))))}function b(){return M("div",{className:"met-queue-summary","data-testid":"met-current-summary"},M(kt,{status:"staged"},`待启动 ${E.staged??0}`),M(kt,{status:"queued"},`排队中 ${E.queued??0}`),M(kt,{status:"running"},`训练中 ${E.running??0}`),M("span",null,`最大并发 ${n.summary?.queue?.maxConcurrency??n.queue?.queue?.maxConcurrency??_.maxConcurrency}`),M("span",null,`目标 GPU ${n.summary?.queue?.targetGpuName??n.queue?.queue?.targetGpuName??_.targetGpuName}`))}function o(P,e){let uu=_.expandedProjectDirs[P];return uu===void 0?e<2:Boolean(uu)}function g(P,e){let uu=o(P,e);A({expandedProjectDirs:{..._.expandedProjectDirs,[P]:!uu}})}function x(P){let e=8+Math.max(0,P.depth)*16;if(Boolean(P.project)){let s=P.project,Nu=Boolean(_.selectedProjects[s.projectPath]),Eu=y.kind==="project"&&y.key===s.projectPath;return M("div",{key:P.path,className:`met-tree-row project ${Nu?"selected":""} ${Eu?"active":""}`,style:{paddingLeft:e},onClick:()=>q(s),"data-testid":`met-project-node-${fc(s.projectPath)}`},M("div",{className:"met-tree-name"},M("input",{type:"checkbox",checked:Nu,onClick:(Hu)=>Hu.stopPropagation(),onChange:(Hu)=>A({selectedProjects:{..._.selectedProjects,[s.projectPath]:Hu.target.checked}}),"data-testid":`met-project-checkbox-${fc(s.projectPath)}`}),M("button",{type:"button",className:"met-inline-link project-path",onClick:(Hu)=>{Hu.stopPropagation(),q(s)}},P.name)),M("span",null,s.useModel||"--"),M("span",null,s.epochTrain??"--"),M("span",null,vt(s.progress?.progressPercent)),M("span",null,lc(s.progress?.epochPerHour)))}let Ku=o(P.path,P.depth);return M(gt.default.Fragment,{key:P.path},M("div",{className:"met-tree-row folder",style:{paddingLeft:e},"data-testid":`met-project-folder-${fc(P.path)}`},M("button",{type:"button",className:"met-tree-toggle",onClick:()=>g(P.path,P.depth),"aria-label":Ku?`折叠 ${P.path}`:`展开 ${P.path}`},Ku?"-":"+"),M("strong",null,P.name),M("span",{className:"met-tree-count"},`${P.count} projects`)),Ku?P.children.map((s)=>x(s)):null)}function lu(P){return M("div",{className:"met-detail-kv"},P.map((e)=>M("div",{key:e.label,className:"met-detail-kv-item"},M("span",null,e.label),M("strong",null,It(e.value)),e.hint?M("small",null,e.hint):null)))}function _u(P,e){return M("div",{className:"met-detail-section"},M("h3",null,P),lu(e))}function $u(P){if(!Array.isArray(P)||P.length===0)return M(p0,{title:"模型层未上报",text:"等待 data/model_info.json 或 compute_analysis.json 生成。"});return M("div",{className:"table-wrap met-layer-table"},M("table",null,M("thead",null,M("tr",null,M("th",null,"Layer"),M("th",null,"Type"),M("th",null,"Params"),M("th",null,"Trainable"),M("th",null,"Compute"))),M("tbody",null,P.slice(0,18).map((e,uu)=>M("tr",{key:`${e.name||"layer"}-${uu}`},M("td",null,e.name||`#${uu+1}`),M("td",null,e.type||"--"),M("td",null,$2(e.num_params)),M("td",null,e.trainable===void 0?"--":String(Boolean(e.trainable))),M("td",null,$2(e.compute?.total??e.estimated_cost?.weighted_units?.total)))))))}function ju(P){let e=Array.isArray(P)?P:[];if(e.length===0)return M(p0,{title:"data/ 暂无文件",text:"训练或评估完成后会生成 training_state、metrics、model_info 等文件。"});return M("div",{className:"met-file-chip-grid"},e.slice(0,48).map((uu)=>M("span",{key:uu},uu)),e.length>48?M("span",null,`+${e.length-48}`):null)}function zu(P){let e=String(P||"").replace(/\x1b\[[0-9;]*[A-Za-z]/g,"").split(/\r?\n/).map((uu)=>uu.trim()).filter(Boolean).slice(-12);if(e.length===0)return M(p0,{title:"暂无日志尾部",text:"该任务未上报 logTail 或日志已轮转。"});return M("div",{className:"met-log-lines"},e.map((uu,Ku)=>M("div",{key:`${Ku}-${uu.slice(0,16)}`},uu)))}function Wu(){if(y.loading)return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},"Detail Loading"),M(nl,{title:"详情加载中",loading:!0}))),M(p0,{title:"详情加载中",text:y.title||"正在读取 D601 data/ 和 config.json"}));if(y.error)return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M(il,{error:y.error,wide:!0}));if(!y.data)return M("section",{className:"met-detail-panel muted","data-testid":"met-detail-panel"},M(p0,{title:"选择一个项目或任务查看详情",text:"项目库、当前队列、已完成和失败诊断中的行都可以点击;默认只展示结构化字段,原始 JSON 需显式点击按钮。"}));let P=uO(y),e=lO(y),uu=If(P.config),Ku=If(P.progress||e.progress),s=If(P.data),Nu=If(P.metrics||s.metrics||Ku.trainingInfo?.evaluation_metrics),Eu=If(s.trainingInfo||Ku.trainingInfo),Hu=If(s.trainingState),vu=If(P.model||s.model),ul=Array.isArray(vu.modelSummary)&&vu.modelSummary.length>0?vu.modelSummary:vu.computeLayers,mu=If(Eu.evaluation_metrics),Fl=y.kind==="job"?"训练任务详情":"Project 详情";return M("section",{className:"met-detail-panel","data-testid":"met-detail-panel"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},y.kind==="job"?"Job + Project Detail":"Project Library Detail"),M(nl,{title:Fl}),M("code",null,P.projectPath||e.projectPath||y.title)),M("div",{className:"panel-actions"},M(v1,{title:`MET ${Fl}`,data:y.data,onOpen:l,testId:"raw-met-detail"}))),y.kind==="job"?_u("任务状态",[{label:"Job ID",value:e.id},{label:"状态",value:kQ(e.status)},{label:"GPU",value:e.gpuName},{label:"Exit Code",value:e.exitCode},{label:"耗时",value:bQ(e)},{label:"训练速度",value:lc(vQ({...e,progress:Ku}))}]):null,_u("config.json",[{label:"use_model",value:uu.use_model},{label:"epoch_train",value:uu.epoch_train},{label:"step_per_epoch",value:uu.step_per_epoch},{label:"learning_rate",value:uu.learning_rate},{label:"using_gpu",value:uu.using_gpu},{label:"use_points",value:uu.use_points},{label:"sample_rate",value:uu.sample_rate},{label:"time_clipped_s",value:uu.time_clipped_s},{label:"H_UNITS",value:uu.H_UNITS},{label:"INNER_KAN_UNITS",value:uu.INNER_KAN_UNITS},{label:"INNER_KAN_LAYERS",value:uu.INNER_KAN_LAYERS},{label:"GRID_SIZE",value:uu.GRID_SIZE},{label:"SPLINE_ORDER",value:uu.SPLINE_ORDER},{label:"USE_FAST_MODEL",value:uu.USE_FAST_MODEL},{label:"IIR_TRAINABLE",value:uu.IIR_TRAINABLE}]),_u("data/ 训练状态",[{label:"Epoch",value:`${Ku.currentEpoch??Hu.current_epoch??Hu.completed_epoch??"--"} / ${Ku.epochTarget??uu.epoch_train??"--"}`},{label:"Progress",value:vt(Ku.progressPercent)},{label:"Last Loss",value:Ku.lastLoss??Hu.loss},{label:"Last Val Loss",value:Ku.lastValLoss??Hu.val_loss},{label:"Min Loss",value:Eu.min_loss??Hu.min_loss},{label:"Min Val Loss",value:Eu.min_val_loss??Hu.min_val_loss},{label:"Log Lines",value:Ku.logLineCount},{label:"ETA",value:_2(Ku.etaSeconds??Hu.remaining_time)},{label:"训练速度",value:lc(Ku.epochPerHour??Hu.smoothed_speed)},{label:"Training Alive",value:Hu.training_alive}]),_u("模型参数",[{label:"Model Type",value:vu.modelType??uu.use_model},{label:"Total Params",value:vu.totalParams,hint:vu.totalParams===null||vu.totalParams===void 0?"未上报":"data/model_info.json"},{label:"Trainable",value:vu.trainableParams},{label:"Non-trainable",value:vu.nonTrainableParams},{label:"Compute Cost",value:vu.computeCost},{label:"Estimate Status",value:vu.estimateStatus},{label:"Unsupported Layers",value:vu.unsupportedLayerCount}]),_u("指标",[{label:"train_loss",value:Nu.train_loss??mu.train_loss},{label:"val_loss",value:Nu.val_loss??mu.val_loss},{label:"train_mae",value:Nu.train_mae??mu.train_mae},{label:"val_mae",value:Nu.val_mae??mu.val_mae},{label:"train_afmae",value:Nu.train_afmae??mu.train_afmae},{label:"val_afmae",value:Nu.val_afmae??mu.val_afmae},{label:"freq_drift_hz",value:Nu.freq_drift_hz},{label:"sens_drift_percent",value:Nu.sens_drift_percent},{label:"linearity_percent",value:Nu.linearity_percent},{label:"weights_source",value:Nu.weights_source??mu.weights_source},{label:"lr min/mean/max",value:`${It(Eu.learning_rate_min)} / ${It(Eu.learning_rate_mean)} / ${It(Eu.learning_rate_max)}`}]),M("div",{className:"met-detail-section"},M("h3",null,"模型层"),$u(ul)),M("div",{className:"met-detail-section"},M("h3",null,"data/ 文件"),ju(s.files)),y.kind==="job"?M("div",{className:"met-detail-section"},M("h3",null,"日志尾部"),zu(If(y.data).logTail)):null)}return M("div",{className:"met-page","data-testid":"met-nonlinear-page"},M(t2,{title:"MET Nonlinear 训练编排",eyebrow:"D601 GPU 用户服务",loading:n.loading||n.actionBusy,actions:M("div",{className:"panel-actions"},M("button",{type:"button",className:"ghost-btn",onClick:j,disabled:n.loading,"data-testid":"met-refresh-button"},n.loading?"刷新中":"刷新"),M(v1,{title:"MET Nonlinear 用户服务",data:r,onOpen:l,testId:"raw-met-service"}))},M("div",{className:"findjob-hero"},M("div",null,M("div",{className:"node-version-line"},M(kt,{status:z.providerStatus==="online"?"online":"warn"},z.providerStatus||"unknown"),M("span",null,r.providerId),M("span",null,H.public?"公网暴露":"仅 UniDesk frontend 代理访问")),M("p",{className:"muted paragraph"},r.description)),M("div",{className:"microservice-ref-card"},M("span",null,"Repo"),M("strong",null,Z.url||"--"),M("code",null,Z.commitId||"--")),M("div",{className:"microservice-ref-card"},M("span",null,"D601 Docker"),M("strong",null,`${H.nodeBindHost||"--"}:${H.nodePort||"--"}`),M("code",null,`${Z.composeFile||"--"} / ${Z.containerName||"--"}`))),M(il,{error:n.error,wide:!0}),_.actionMessage?M("div",{className:"met-action-log","data-testid":"met-action-message"},_.actionMessage):null),M("div",{className:"met-grid"},M(t2,{title:"核心状态",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Queue + GPU",loading:n.loading},M("div",{className:"metric-grid"},M(Hn,{label:"Staged",value:E.staged??0,hint:"加入队列未开始",tone:Number(E.staged||0)>0?"warn":""}),M(Hn,{label:"Queued",value:E.queued??0,hint:"排队等待调度",tone:Number(E.queued||0)>0?"warn":""}),M(Hn,{label:"Running",value:E.running??0,hint:`max ${n.summary?.queue?.maxConcurrency??n.queue?.queue?.maxConcurrency??"--"}`,tone:Number(E.running||0)>0?"ok":""}),M(Hn,{label:"Succeeded",value:E.succeeded??0,hint:"已完成"}),M(Hn,{label:"Failed",value:E.failed??0,hint:"需要诊断",tone:Number(E.failed||0)>0?"warn":""}),M(Hn,{label:"2080Ti Free",value:h?vt(Number(h.freeRatio)*100):"--",hint:h?`${h.memoryFreeMiB}/${h.memoryTotalMiB} MiB`:"等待 GPU 上报"}),M(Hn,{label:"ML Image",value:V.present?"READY":"MISSING",hint:V.image||"met-nonlinear-ml:tf26",tone:V.present?"ok":"warn"}),M(Hn,{label:"Health",value:n.health?.ok?"OK":"--",hint:"D601 /health"}))),M(t2,{title:"队列控制",eyebrow:"Downloader-like staging",loading:n.actionBusy},M("div",{className:"met-control-strip"},M("label",null,"最大并发",M("input",{type:"number",min:1,max:16,value:_.maxConcurrency,"data-testid":"met-max-concurrency-input",onChange:(P)=>A({maxConcurrency:P.target.value})})),M("label",null,"目标 GPU",M("input",{value:_.targetGpuName,"data-testid":"met-target-gpu-input",onChange:(P)=>A({targetGpuName:P.target.value})})),M("button",{type:"button",className:"ghost-btn",onClick:J,disabled:n.actionBusy,"data-testid":"met-save-settings-button"},"保存设置"),M("button",{type:"button",className:"primary-btn",onClick:U,disabled:n.actionBusy||Number(E.staged||0)===0,"data-testid":"met-start-queue-button"},"启动队列")),M("p",{className:"muted paragraph"},"Project 先进入待启动队列,不会立即训练;点击启动队列后才切换为排队中,并由 D601 scheduler 按最大并发和 2080Ti 显存策略调度。")),M("section",{className:"panel met-workspace"},M("div",{className:"met-tabs",role:"tablist"},k.map((P)=>M("button",{key:P.id,type:"button",className:_.activeTab===P.id?"active":"",onClick:()=>A({activeTab:P.id}),"data-testid":`met-tab-${P.id}`},`${P.label} ${P.count}`))),M("div",{className:"panel-body"},_.activeTab==="projects"?M("div",{className:"met-form-grid","data-testid":"met-projects-pane"},M("div",{className:"met-fork-card"},M("h3",null,"Fork Project"),M("label",null,"源 Project",M("select",{value:m,"data-testid":"met-source-project-select",onChange:(P)=>A({sourceProject:P.target.value})},p.map((P)=>M("option",{key:P.projectPath,value:P.projectPath},`${P.projectPath} · ${P.useModel||"model?"}`)))),M("label",null,"Fork 数量",M("input",{type:"number",min:1,max:100,value:_.forkCount,"data-testid":"met-fork-count-input",onChange:(P)=>A({forkCount:P.target.value})})),M("label",null,"训练轮数",M("input",{type:"number",min:1,max:1e5,value:_.forkEpochs,"data-testid":"met-fork-epochs-input",onChange:(P)=>A({forkEpochs:P.target.value})})),M("label",null,"目标前缀",M("input",{value:_.forkPrefix,"data-testid":"met-fork-prefix-input",onChange:(P)=>A({forkPrefix:P.target.value})})),M("button",{type:"button",className:"primary-btn",onClick:L,disabled:n.actionBusy||!m,"data-testid":"met-fork-button"},"Fork Project"),M("p",{className:"muted paragraph"},"Fork 只创建新 Project 并自动勾选,不会直接训练;需要在右侧确认后加入待启动队列。")),M("div",{className:"met-project-list"},M("div",{className:"panel-head compact"},M("div",null,M("p",{className:"panel-eyebrow"},`Existing Projects · ${(n.projects?.roots||[]).map((P)=>`${P.root} ${P.count}`).join(" / ")}`),M(nl,{title:"选择已有 Project",loading:n.loading||n.actionBusy})),M("button",{type:"button",className:"ghost-btn",onClick:w,disabled:n.actionBusy||Q().length===0,"data-testid":"met-stage-selected-button"},`加入待启动队列 (${Q().length})`)),p.length===0?M(p0,{title:"暂无 project",text:"等待 D601 返回 /api/projects"}):M("div",{className:"met-project-table","data-testid":"met-project-tree"},M("div",{className:"met-tree-header"},M("span",null,"文件树 Project"),M("span",null,"Model"),M("span",null,"Epochs"),M("span",null,"Progress"),M("span",null,"速度")),O.children.map((P)=>x(P)))),Wu()):null,_.activeTab==="current"?M("div",{"data-testid":"met-current-pane"},b(),I(X,"current"),Wu(),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET Queue",data:n.queue,onOpen:l,testId:"raw-met-queue"}))):null,_.activeTab==="completed"?M("div",{"data-testid":"met-completed-pane"},I(v.length>0?v:Y.filter((P)=>P.status==="succeeded"),"completed"),Wu()):null,_.activeTab==="failed"?M("div",{"data-testid":"met-failed-pane"},I(T.length>0?T:Y.filter((P)=>["failed","canceled"].includes(P.status)),"failed"),Wu(),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET History",data:n.history,onOpen:l,testId:"raw-met-history"}))):null,_.activeTab==="gpu"?M("div",{className:"met-gpu-pane","data-testid":"met-gpu-pane"},D.length===0?M(p0,{title:"暂无 GPU 上报",text:"等待 D601 met-nonlinear-ts 或 ML image 提供 nvidia-smi 数据"}):M("div",{className:"table-wrap"},M("table",null,M("thead",null,M("tr",null,M("th",null,"Index"),M("th",null,"Name"),M("th",null,"Free"),M("th",null,"Policy"))),M("tbody",null,D.map((P)=>M("tr",{key:P.index},M("td",null,P.index),M("td",null,P.name),M("td",null,`${P.memoryFreeMiB} / ${P.memoryTotalMiB} MiB`,M("div",{className:"met-progress"},M("span",{style:{width:vt(Number(P.freeRatio)*100)}}))),M("td",null,String(P.name||"").includes("2080")?"target 2080Ti, <20% 限制并发":"non-target")))))),M("div",{className:"panel-actions inline-actions"},M(v1,{title:"MET Images",data:n.images,onOpen:l,testId:"raw-met-images"}))):null))))}var ic=[{id:"ops",label:"运行总览",code:"OPS",tabs:[{id:"status",label:"态势总览"},{id:"performance",label:"性能面板"},{id:"events",label:"事件摘要"},{id:"logs",label:"服务日志"}]},{id:"nodes",label:"资源节点",code:"NODE",tabs:[{id:"list",label:"节点清单"},{id:"monitor",label:"资源监控"},{id:"docker",label:"Docker 状态"},{id:"gateway",label:"网关版本"},{id:"labels",label:"资源标签"},{id:"heartbeats",label:"心跳状态"}]},{id:"tasks",label:"任务调度",code:"TASK",tabs:[{id:"dispatch",label:"下发任务"},{id:"scheduled",label:"定时任务"},{id:"pending",label:"待处理任务"},{id:"history",label:"任务历史"},{id:"results",label:"执行结果"}]},{id:"apps",label:"用户服务",code:"APP",routeSegment:"app",tabs:[{id:"catalog",label:"服务目录"},{id:"todo-note",label:"Todo Note"},{id:"findjob",label:"FindJob"},{id:"pipeline",label:"Pipeline"},{id:"met-nonlinear",label:"MET Nonlinear"},{id:"claudeqq",label:"ClaudeQQ"},{id:"baidu-netdisk",label:"Baidu Netdisk"},{id:"filebrowser",label:"File Browser"},{id:"oa-event-flow",label:"OA Event Flow"},{id:"k3sctl",label:"K3S Control"},{id:"code-queue",label:"Code Queue"},{id:"project-manager",label:"Project Manager"}]},{id:"config",label:"系统配置",code:"CFG",tabs:[{id:"topology",label:"连接拓扑"},{id:"auth",label:"认证策略"},{id:"security",label:"安全边界"}]}],st=Object.fromEntries(ic.map((u)=>[u.id,u.tabs[0]?.id??""]));function fO(u){let l=String(u||"").trim();if(!l)return"";try{return decodeURIComponent(l)}catch{return l}}function nc(u){let l=String(u||"/"),[f]=l.split(/[?#]/u,1);if(f==="/")return"/";let n=`/${f.split("/").map(fO).filter(Boolean).join("/")}`;return n.endsWith("/")?n:`${n}/`}function rO(u){let l=2166136261;for(let f of u)l^=f.charCodeAt(0),l=Math.imul(l,16777619);return Math.abs(l>>>0).toString(36)}function c2(u){return String(u||"").normalize("NFKD").replace(/[\u0300-\u036f]/gu,"").toLowerCase().replace(/[^a-z0-9]+/gu,"-").replace(/^-+|-+$/gu,"")}function sQ(u){return String(u||"").trim().toLowerCase().replace(/[\s/\\?#%]+/gu,"-").replace(/-+/gu,"-").replace(/^-+|-+$/gu,"")}function aQ(u){let l=c2(u.routeSegment||"")||sQ(u.routeSegment||"");if(l)return l;let f=c2(u.id||"");if(f)return f;let r=c2(u.label||"")||sQ(u.label||"");if(r)return r;return`route-${rO(JSON.stringify(u))}`}function A2(u,l){return`${u}:${l}`}function oQ(u){let l=u.map((_)=>{let c=aQ(_);return{..._,routeSegment:c,tabs:_.tabs.map((A)=>({...A,routeSegment:aQ(A)}))}}),f={},r={},n={},i=l.map((_)=>{let c=_.tabs[0]?.id??"";n[_.id]=c;let A=_.tabs.map((J)=>{let Q=`/${_.routeSegment}/${J.routeSegment}/`,w=[Q],L={moduleId:_.id,tabId:J.id};for(let U of w)f[nc(U)]=L;return r[A2(_.id,J.id)]=Q,{...J,canonicalPath:Q,aliases:w}}),j=`/${_.routeSegment}/`,F={moduleId:_.id,tabId:c};return f[nc(j)]=F,{..._,routeSegment:_.routeSegment,canonicalPath:j,tabs:A}}),y=i[0],t={moduleId:y?.id||"",tabId:y?.tabs[0]?.id||""};return f["/"]=t,{modules:i,moduleById:Object.fromEntries(i.map((_)=>[_.id,_])),defaultActiveTabs:n,routeMap:f,canonicalPathByTarget:r,fallbackTarget:t}}function j2(u,l){return u.routeMap[nc(l)]||u.fallbackTarget}function yc(u,l,f){return u.canonicalPathByTarget[A2(l,f)]||u.canonicalPathByTarget[A2(u.fallbackTarget.moduleId,u.fallbackTarget.tabId)]||"/"}function dQ(u,l){let f=u.routeMap[nc(l)];if(!f)return null;return yc(u,f.moduleId,f.tabId)}var cc=Pu(Jl(),1);var iu=cc.default.createElement,{useEffect:eQ,useMemo:nO}=cc.default,F2=cc.default.useState;function $c({status:u,children:l,title:f}){let r=String(u||"unknown").toLowerCase();return iu("span",{className:`status-badge ${r}`,title:f},l||u||"unknown")}function at({label:u,value:l,hint:f,tone:r}){return iu("article",{className:`metric-card ${r||""}`},iu("div",{className:"metric-label"},u),iu("div",{className:"metric-value"},l),iu("div",{className:"metric-hint"},f))}function tc({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return iu("section",{className:`panel ${n||""}`},iu("div",{className:"panel-head"},iu("div",null,l?iu("p",{className:"panel-eyebrow"},l):null,iu(nl,{title:u,loading:i})),f?iu("div",{className:"panel-actions"},f):null),iu("div",{className:"panel-body"},r))}function ot({title:u,data:l,onOpen:f,testId:r}){return iu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f?.(u,l)},"查看原始JSON")}function U2({title:u,text:l}){return iu("div",{className:"empty-state"},iu("strong",null,u),iu("span",null,l))}function iO(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:null}function m0(u){return Array.isArray(u)?u:[]}function gf(u){let l=Number(u);return Number.isFinite(l)?l.toLocaleString("zh-CN"):"--"}function uN(u,l=140){if(u===null||u===void 0)return"--";let f=typeof u==="string"?u:JSON.stringify(u),r=String(f||"").replace(/\s+/gu," ").trim();return r.length>l?`${r.slice(0,l-1)}...`:r||"--"}function yO(u){return m0(u?.tags).map((l)=>String(l||"").trim()).filter(Boolean)}function Zi(u){let l=Number(u);return Number.isFinite(l)&&l>=0?Math.floor(l):0}function tO(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function _O(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function $O(u){return String(u||"").split(/[\s,]+/u).map((l)=>l.trim()).filter(Boolean).join(",")}function _c(u,l){return`${u}/microservices/oa-event-flow/proxy${l}`}function cO(u){if(u.includes("error")||u.includes("failed"))return"failed";if(u.includes("stats"))return"ok";if(u.includes("step")||u.includes("updated"))return"running";return"queued"}function AO(u){let l=String(u?.subjectKind||"trace"),f=String(u?.subjectId||u?.scopeId||"");return f?`${l}:${f}`:String(u?.scopeId||"--")}function jO({tags:u}){let l=yO({tags:u}).slice(0,6);return iu("div",{className:"oa-tag-rail"},l.length===0?iu("span",{className:"muted"},"--"):l.map((f)=>iu("code",{key:f},f)))}function FO({events:u,onRaw:l}){let f=[...m0(u)].reverse();return f.length===0?iu(U2,{title:"事件表暂无记录",text:"等待 Code Queue 或 Pipeline 按 tag 发布 OA 事件"}):iu("div",{className:"table-wrap oa-event-table-wrap"},iu("table",{className:"oa-event-table","data-testid":"oa-event-flow-event-table"},iu("thead",null,iu("tr",null,iu("th",null,"Seq"),iu("th",null,"Type"),iu("th",null,"Source"),iu("th",null,"Aggregate"),iu("th",null,"Tags"),iu("th",null,"Payload"),iu("th",null,"Created"),iu("th",null,"Raw"))),iu("tbody",null,f.map((r)=>{let n=String(r?.type||"event"),i=`${String(r?.aggregateType||"--")}:${String(r?.aggregateId||"--")}`;return iu("tr",{key:r?.eventId||r?.sequence},iu("td",null,iu("code",null,gf(r?.sequence))),iu("td",null,iu($c,{status:cO(n)},n)),iu("td",null,iu("strong",null,r?.sourceId||"--"),iu("code",null,r?.sourceKind||"--")),iu("td",null,iu("code",null,i)),iu("td",null,iu(jO,{tags:r?.tags})),iu("td",null,iu("span",{className:"oa-payload-preview"},uN(r?.payload,180))),iu("td",null,qu(r?.createdAt)),iu("td",null,iu(ot,{title:`OA Event ${r?.sequence||""}`,data:r,onOpen:l,testId:`raw-oa-event-${r?.sequence||"unknown"}`})))}))))}function UO({stats:u,onRaw:l}){let f=m0(u);return f.length===0?iu(U2,{title:"统计中心暂无投影",text:"trace-stats-snapshot / trace-step-created 进入事件流后会更新这里"}):iu("div",{className:"table-wrap oa-stats-table-wrap"},iu("table",{className:"oa-stats-table","data-testid":"oa-event-flow-stats"},iu("thead",null,iu("tr",null,iu("th",null,"Scope"),iu("th",null,"Service"),iu("th",null,"STEP"),iu("th",null,"Read"),iu("th",null,"Edit"),iu("th",null,"Run"),iu("th",null,"Error"),iu("th",null,"Output Seq"),iu("th",null,"Revision"),iu("th",null,"Updated"),iu("th",null,"Raw"))),iu("tbody",null,f.map((r)=>iu("tr",{key:r?.scopeId||`${r?.serviceId}-${r?.subjectId}`},iu("td",null,iu("strong",null,AO(r)),iu("code",null,r?.scopeId||"--")),iu("td",null,iu($c,{status:String(r?.serviceId||"unknown")==="code-queue"?"running":"queued"},r?.serviceId||"--")),iu("td",null,iu("strong",null,gf(Zi(r?.stepCount??r?.llmStepCount)))),iu("td",null,gf(Zi(r?.readCount))),iu("td",null,gf(Zi(r?.editCount))),iu("td",null,gf(Zi(r?.runCount))),iu("td",null,gf(Zi(r?.errorCount))),iu("td",null,iu("code",null,gf(Zi(r?.outputMaxSeq)))),iu("td",null,gf(Zi(r?.statsRevision))),iu("td",null,qu(r?.updatedAt)),iu("td",null,iu(ot,{title:`OA Trace Stats ${r?.scopeId||""}`,data:r,onOpen:l,testId:`raw-oa-stats-${String(r?.scopeId||"unknown").replace(/[^a-zA-Z0-9_-]/gu,"_")}`})))))))}function lN({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((z)=>z.id==="oa-event-flow")||null,[n,i]=F2("service:code-queue"),[y,t]=F2({loading:!1,error:"",health:null,diagnostics:null,events:[],stats:[],refreshedAt:null}),[_,c]=F2({status:"idle",message:"未连接",lastEventAt:""}),A=nO(()=>$O(n),[n]);async function j(){if(!r)return;t((z)=>({...z,loading:!0,error:""}));try{let z=A?`tags=${encodeURIComponent(A)}&`:"",[Z,H,E,D]=await Promise.all([Tu(`${f}/microservices/oa-event-flow/health`,{failureFields:[]}),Tu(_c(f,"/api/diagnostics")),Tu(_c(f,`/api/events?${z}limit=100`)),Tu(_c(f,`/api/stats/trace?${z}limit=100`))]);t({loading:!1,error:"",health:Z,diagnostics:H,events:m0(E?.events),stats:m0(D?.stats),refreshedAt:new Date})}catch(z){t((Z)=>({...Z,loading:!1,error:Ou(z,"OA Event Flow 加载失败")}))}}if(eQ(()=>{j()},[r?.id,r?.runtime?.providerStatus,A]),eQ(()=>{if(!r||typeof EventSource>"u")return;let z=A?`?tags=${encodeURIComponent(A)}`:"",Z=new EventSource(`${_c(f,"/api/events/stream")}${z}`,{withCredentials:!0});c({status:"running",message:"SSE connecting",lastEventAt:""});let H=(h)=>{c({status:"online",message:uN(h.data,120),lastEventAt:new Date().toISOString()})},E=(h)=>{try{let V=JSON.parse(String(h.data||"{}"));c({status:"online",message:String(V?.type||h.type||"event"),lastEventAt:new Date().toISOString()}),t((S)=>{let p=[...m0(S.events).filter((m)=>String(m?.eventId||"")!==String(V?.eventId||"")),V].sort((m,X)=>Number(m?.sequence||0)-Number(X?.sequence||0)).slice(-100),O=V?.type==="trace-stats-updated"&&iO(V?.payload?.stats)?[V.payload.stats,...m0(S.stats).filter((m)=>String(m?.scopeId||"")!==String(V.payload.stats.scopeId||""))].slice(0,100):S.stats;return{...S,events:p,stats:O}})}catch(V){c({status:"warn",message:Ou(V,"SSE 事件解析失败"),lastEventAt:new Date().toISOString()})}},D=()=>{c((h)=>({...h,status:"warn",message:"SSE reconnecting"}))};return Z.addEventListener("hello",H),Z.addEventListener("task-updated",E),Z.addEventListener("queue-updated",E),Z.addEventListener("trace-step-created",E),Z.addEventListener("trace-stats-snapshot",E),Z.addEventListener("trace-stats-updated",E),Z.addEventListener("trace-error",E),Z.onerror=D,()=>Z.close()},[r?.id,f,A]),!r)return iu(U2,{title:"OA Event Flow 未登记",text:"请在 config.json 的 microservices 中登记 id=oa-event-flow"});let F=tO(r),J=_O(r),Q=y.diagnostics||{},w=y.health||{},L=Q.eventCount??w.eventCount,U=Q.traceStatsCount??w.traceStatsCount,N=Q.latestSequence??w.latestSequence,q=Q.pipelineBridge||w.pipelineBridge||{},W=m0(Q.eventTypes).slice(0,8);return iu("div",{className:"oa-event-flow-page","data-testid":"oa-event-flow-page"},iu(tc,{title:"OA Event Flow 控制台",eyebrow:"Unified OA Event Bus + Stats Projection",loading:y.loading,actions:iu("div",{className:"panel-actions"},iu("button",{type:"button",className:"ghost-btn",onClick:j,disabled:y.loading,"data-testid":"oa-event-flow-refresh"},y.loading?"刷新中":"刷新"),iu(ot,{title:"OA Event Flow Service",data:r,onOpen:l,testId:"raw-oa-event-flow-service"}))},iu("div",{className:"oa-flow-hero"},iu("div",null,iu("div",{className:"node-version-line"},iu($c,{status:w?.ok||F.providerStatus==="online"?"online":"warn"},w?.ok?"HEALTH OK":F.providerStatus||"unknown"),iu($c,{status:_.status},_.status.toUpperCase()),iu("span",null,J.public?"公网暴露":"仅 UniDesk frontend 代理访问")),iu("p",{className:"muted paragraph"},"独立事件流微服务统一承载 Code Queue 与 Pipeline 的事件发布、tag 订阅、事件表审计和 Trace/STEP 统计投影。")),iu("div",{className:"oa-flow-signal"},iu("span",null,"stream"),iu("strong",null,_.message||"--"),iu("code",null,_.lastEventAt?tl(new Date(_.lastEventAt)):"waiting"))),iu(il,{error:y.error,wide:!0})),iu("div",{className:"oa-flow-metrics"},iu(at,{label:"事件总量",value:gf(L),hint:`latest seq ${gf(N)}`,tone:"ok"}),iu(at,{label:"Trace Stats",value:gf(U),hint:"oa_trace_stats 投影"}),iu(at,{label:"SSE Clients",value:gf(w?.sseClientCount??m0(Q.sseClients).length),hint:_.message||"tag subscription"}),iu(at,{label:"Pipeline Bridge",value:q?.enabled?gf(q?.insertedCount):"OFF",hint:q?.lastError||q?.lastFinishedAt||`${q?.mode||"snapshot"} service:pipeline`}),iu(at,{label:"DB",value:w?.databaseReady||Q.databaseReady?"READY":"WAIT",hint:w?.databaseLastError||Q.databaseLastError||"PostgreSQL persisted"})),iu(tc,{title:"标签订阅",eyebrow:y.refreshedAt?`Updated ${tl(y.refreshedAt)}`:"Tag Pub/Sub"},iu("div",{className:"oa-filter-bar"},iu("label",null,iu("span",null,"tags"),iu("input",{value:n,onChange:(z)=>i(z.target.value),placeholder:"service:code-queue, trace","data-testid":"oa-event-flow-tag-filter"})),iu("div",{className:"oa-filter-presets"},iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("service:code-queue")},"Code Queue"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("service:pipeline")},"Pipeline"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("trace")},"Trace"),iu("button",{type:"button",className:"ghost-btn",onClick:()=>i("")},"All")),iu("code",null,A||"all events")),iu("div",{className:"oa-type-strip"},W.length===0?iu("span",{className:"muted"},"等待事件类型统计"):W.map((z)=>iu("span",{key:z.type,className:"data-chip"},`${z.type} ${gf(z.count)}`)))),iu("div",{className:"oa-flow-grid"},iu(tc,{title:"事件表",eyebrow:"oa_events persisted log",className:"oa-flow-wide",loading:y.loading,actions:iu(ot,{title:"OA Event Query",data:{events:y.events,diagnostics:Q},onOpen:l,testId:"raw-oa-events"})},iu(FO,{events:y.events,onRaw:l})),iu(tc,{title:"统计中心",eyebrow:"oa_trace_stats read model",className:"oa-flow-wide",loading:y.loading,actions:iu(ot,{title:"OA Trace Stats",data:y.stats,onOpen:l,testId:"raw-oa-trace-stats"})},iu(UO,{stats:y.stats,onRaw:l}))))}var hn=Pu(Jl(),1);var ru=Pu(iN(),1),yu=Pu(Jl(),1);function Sl(u){if(typeof u==="string"||typeof u==="number")return""+u;let l="";if(Array.isArray(u)){for(let f=0,r;f{}};function tN(){for(var u=0,l=arguments.length,f={},r;u=0)r=f.slice(n+1),f=f.slice(0,n);if(f&&!l.hasOwnProperty(f))throw Error("unknown type: "+f);return{type:f,name:r}})}jc.prototype=tN.prototype={constructor:jc,on:function(u,l){var f=this._,r=KO(u+"",f),n,i=-1,y=r.length;if(arguments.length<2){while(++i0)for(var f=Array(n),r=0,n,i;r=0&&(l=u.slice(0,f))!=="xmlns")u=u.slice(f+1);return J2.hasOwnProperty(l)?{space:J2[l],local:u}:u}function Q2(u){let l;while(l=u.sourceEvent)u=l;return u}function Gf(u,l){if(u=Q2(u),l===void 0)l=u.currentTarget;if(l){var f=l.ownerSVGElement||l;if(f.createSVGPoint){var r=f.createSVGPoint();return r.x=u.clientX,r.y=u.clientY,r=r.matrixTransform(l.getScreenCTM().inverse()),[r.x,r.y]}if(l.getBoundingClientRect){var n=l.getBoundingClientRect();return[u.clientX-n.left-l.clientLeft,u.clientY-n.top-l.clientTop]}}return[u.pageX,u.pageY]}function zO(){}function Bn(u){return u==null?zO:function(){return this.querySelector(u)}}function N2(u){if(typeof u!=="function")u=Bn(u);for(var l=this._groups,f=l.length,r=Array(f),n=0;n=q)q=N+1;while(!(z=L[q])&&++q=0;)if(y=r[n]){if(i&&y.compareDocumentPosition(i)^4)i.parentNode.insertBefore(y,i);i=y}return this}function B2(u){if(!u)u=mO;function l(j,F){return j&&F?u(j.__data__,F.__data__):!j-!F}for(var f=this._groups,r=f.length,n=Array(r),i=0;il?1:u>=l?0:NaN}function V2(){var u=arguments[0];return arguments[0]=this,u.apply(null,arguments),this}function D2(){return Array.from(this)}function X2(){for(var u=this._groups,l=0,f=u.length;l1?this.each((l==null?bO:typeof l==="function"?kO:vO)(u,l,f==null?"":f)):Vn(this.node(),u)}function Vn(u,l){return u.style.getPropertyValue(l)||u_(u).getComputedStyle(u,null).getPropertyValue(l)}function IO(u){return function(){delete this[u]}}function gO(u,l){return function(){this[u]=l}}function sO(u,l){return function(){var f=l.apply(this,arguments);if(f==null)delete this[u];else this[u]=f}}function C2(u,l){return arguments.length>1?this.each((l==null?IO:typeof l==="function"?sO:gO)(u,l)):this.node()[u]}function _N(u){return u.trim().split(/^|\s+/)}function M2(u){return u.classList||new $N(u)}function $N(u){this._node=u,this._names=_N(u.getAttribute("class")||"")}$N.prototype={add:function(u){var l=this._names.indexOf(u);if(l<0)this._names.push(u),this._node.setAttribute("class",this._names.join(" "))},remove:function(u){var l=this._names.indexOf(u);if(l>=0)this._names.splice(l,1),this._node.setAttribute("class",this._names.join(" "))},contains:function(u){return this._names.indexOf(u)>=0}};function cN(u,l){var f=M2(u),r=-1,n=l.length;while(++r=0)f=l.slice(r+1),l=l.slice(0,r);return{type:l,name:f}})}function JH(u){return function(){var l=this.__on;if(!l)return;for(var f=0,r=-1,n=l.length,i;f()=>u;function n_(u,{sourceEvent:l,subject:f,target:r,identifier:n,active:i,x:y,y:t,dx:_,dy:c,dispatch:A}){Object.defineProperties(this,{type:{value:u,enumerable:!0,configurable:!0},sourceEvent:{value:l,enumerable:!0,configurable:!0},subject:{value:f,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:n,enumerable:!0,configurable:!0},active:{value:i,enumerable:!0,configurable:!0},x:{value:y,enumerable:!0,configurable:!0},y:{value:t,enumerable:!0,configurable:!0},dx:{value:_,enumerable:!0,configurable:!0},dy:{value:c,enumerable:!0,configurable:!0},_:{value:A}})}n_.prototype.on=function(){var u=this._.on.apply(this._,arguments);return u===this._?this:u};function ZH(u){return!u.ctrlKey&&!u.button}function OH(){return this.parentNode}function HH(u,l){return l==null?{x:u.x,y:u.y}:l}function BH(){return navigator.maxTouchPoints||"ontouchstart"in this}function i_(){var u=ZH,l=OH,f=HH,r=BH,n={},i=Oi("start","drag","end"),y=0,t,_,c,A,j=0;function F(W){W.on("mousedown.drag",J).filter(r).on("touchstart.drag",L).on("touchmove.drag",U,UN).on("touchend.drag touchcancel.drag",N).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function J(W,z){if(A||!u.call(this,W,z))return;var Z=q(this,l.call(this,W,z),W,z,"mouse");if(!Z)return;sl(W.view).on("mousemove.drag",Q,Hi).on("mouseup.drag",w,Hi),g1(W.view),Jc(W),c=!1,t=W.clientX,_=W.clientY,Z("start",W)}function Q(W){if(C0(W),!c){var z=W.clientX-t,Z=W.clientY-_;c=z*z+Z*Z>j}n.mouse("drag",W)}function w(W){sl(W.view).on("mousemove.drag mouseup.drag",null),f_(W.view,c),C0(W),n.mouse("end",W)}function L(W,z){if(!u.call(this,W,z))return;var Z=W.changedTouches,H=l.call(this,W,z),E=Z.length,D,h;for(D=0;D>8&15|l>>4&240,l>>4&15|l&240,(l&15)<<4|l&15,1):f===8?Qc(l>>24&255,l>>16&255,l>>8&255,(l&255)/255):f===4?Qc(l>>12&15|l>>8&240,l>>8&15|l>>4&240,l>>4&15|l&240,((l&15)<<4|l&15)/255):null):(l=DH.exec(u))?new sf(l[1],l[2],l[3],1):(l=XH.exec(u))?new sf(l[1]*255/100,l[2]*255/100,l[3]*255/100,1):(l=SH.exec(u))?Qc(l[1],l[2],l[3],l[4]):(l=YH.exec(u))?Qc(l[1]*255/100,l[2]*255/100,l[3]*255/100,l[4]):(l=pH.exec(u))?LN(l[1],l[2]/100,l[3]/100,1):(l=mH.exec(u))?LN(l[1],l[2]/100,l[3]/100,l[4]):JN.hasOwnProperty(u)?qN(JN[u]):u==="transparent"?new sf(NaN,NaN,NaN,0):null}function qN(u){return new sf(u>>16&255,u>>8&255,u&255,1)}function Qc(u,l,f,r){if(r<=0)u=l=f=NaN;return new sf(u,l,f,r)}function MH(u){if(!(u instanceof $_))u=kr(u);if(!u)return new sf;return u=u.rgb(),new sf(u.r,u.g,u.b,u.opacity)}function a1(u,l,f,r){return arguments.length===1?MH(u):new sf(u,l,f,r==null?1:r)}function sf(u,l,f,r){this.r=+u,this.g=+l,this.b=+f,this.opacity=+r}y_(sf,a1,l5($_,{brighter(u){return u=u==null?qc:Math.pow(qc,u),new sf(this.r*u,this.g*u,this.b*u,this.opacity)},darker(u){return u=u==null?t_:Math.pow(t_,u),new sf(this.r*u,this.g*u,this.b*u,this.opacity)},rgb(){return this},clamp(){return new sf(Vi(this.r),Vi(this.g),Vi(this.b),Wc(this.opacity))},displayable(){return-0.5<=this.r&&this.r<255.5&&(-0.5<=this.g&&this.g<255.5)&&(-0.5<=this.b&&this.b<255.5)&&(0<=this.opacity&&this.opacity<=1)},hex:WN,formatHex:WN,formatHex8:RH,formatRgb:wN,toString:wN}));function WN(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}`}function RH(){return`#${Bi(this.r)}${Bi(this.g)}${Bi(this.b)}${Bi((isNaN(this.opacity)?1:this.opacity)*255)}`}function wN(){let u=Wc(this.opacity);return`${u===1?"rgb(":"rgba("}${Vi(this.r)}, ${Vi(this.g)}, ${Vi(this.b)}${u===1?")":`, ${u})`}`}function Wc(u){return isNaN(u)?1:Math.max(0,Math.min(1,u))}function Vi(u){return Math.max(0,Math.min(255,Math.round(u)||0))}function Bi(u){return u=Vi(u),(u<16?"0":"")+u.toString(16)}function LN(u,l,f,r){if(r<=0)u=l=f=NaN;else if(f<=0||f>=1)u=l=NaN;else if(l<=0)u=NaN;return new vr(u,l,f,r)}function GN(u){if(u instanceof vr)return new vr(u.h,u.s,u.l,u.opacity);if(!(u instanceof $_))u=kr(u);if(!u)return new vr;if(u instanceof vr)return u;u=u.rgb();var l=u.r/255,f=u.g/255,r=u.b/255,n=Math.min(l,f,r),i=Math.max(l,f,r),y=NaN,t=i-n,_=(i+n)/2;if(t){if(l===i)y=(f-r)/t+(f0&&_<1?0:y;return new vr(y,t,_,u.opacity)}function zN(u,l,f,r){return arguments.length===1?GN(u):new vr(u,l,f,r==null?1:r)}function vr(u,l,f,r){this.h=+u,this.s=+l,this.l=+f,this.opacity=+r}y_(vr,zN,l5($_,{brighter(u){return u=u==null?qc:Math.pow(qc,u),new vr(this.h,this.s,this.l*u,this.opacity)},darker(u){return u=u==null?t_:Math.pow(t_,u),new vr(this.h,this.s,this.l*u,this.opacity)},rgb(){var u=this.h%360+(this.h<0)*360,l=isNaN(u)||isNaN(this.s)?0:this.s,f=this.l,r=f+(f<0.5?f:1-f)*l,n=2*f-r;return new sf(f5(u>=240?u-240:u+120,n,r),f5(u,n,r),f5(u<120?u+240:u-120,n,r),this.opacity)},clamp(){return new vr(KN(this.h),Nc(this.s),Nc(this.l),Wc(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&(0<=this.l&&this.l<=1)&&(0<=this.opacity&&this.opacity<=1)},formatHsl(){let u=Wc(this.opacity);return`${u===1?"hsl(":"hsla("}${KN(this.h)}, ${Nc(this.s)*100}%, ${Nc(this.l)*100}%${u===1?")":`, ${u})`}`}}));function KN(u){return u=(u||0)%360,u<0?u+360:u}function Nc(u){return Math.max(0,Math.min(1,u||0))}function f5(u,l,f){return(u<60?l+(f-l)*u/60:u<180?f:u<240?l+(f-l)*(240-u)/60:l)*255}function r5(u,l,f,r,n){var i=u*u,y=i*u;return((1-3*u+3*i-y)*l+(4-6*i+3*y)*f+(1+3*u+3*i-3*y)*r+y*n)/6}function n5(u){var l=u.length-1;return function(f){var r=f<=0?f=0:f>=1?(f=1,l-1):Math.floor(f*l),n=u[r],i=u[r+1],y=r>0?u[r-1]:2*n-i,t=r()=>u;function hH(u,l){return function(f){return u+f*l}}function bH(u,l,f){return u=Math.pow(u,f),l=Math.pow(l,f)-u,f=1/f,function(r){return Math.pow(u+r*l,f)}}function TN(u){return(u=+u)===1?Lc:function(l,f){return f-l?bH(l,f,u):c_(isNaN(l)?f:l)}}function Lc(u,l){var f=l-u;return f?hH(u,f):c_(isNaN(u)?l:u)}var Di=function u(l){var f=TN(l);function r(n,i){var y=f((n=a1(n)).r,(i=a1(i)).r),t=f(n.g,i.g),_=f(n.b,i.b),c=Lc(n.opacity,i.opacity);return function(A){return n.r=y(A),n.g=t(A),n.b=_(A),n.opacity=c(A),n+""}}return r.gamma=u,r}(1);function EN(u){return function(l){var f=l.length,r=Array(f),n=Array(f),i=Array(f),y,t;for(y=0;yf)if(i=l.slice(f,i),t[y])t[y]+=i;else t[++y]=i;if((r=r[0])===(n=n[0]))if(t[y])t[y]+=n;else t[++y]=n;else t[++y]=null,_.push({i:y,x:zf(r,n)});f=$5.lastIndex}if(f180)A+=360;else if(A-c>180)c+=360;F.push({i:j.push(n(j)+"rotate(",null,r)-2,x:zf(c,A)})}else if(A)j.push(n(j)+"rotate("+A+r)}function t(c,A,j,F){if(c!==A)F.push({i:j.push(n(j)+"skewX(",null,r)-2,x:zf(c,A)});else if(A)j.push(n(j)+"skewX("+A+r)}function _(c,A,j,F,J,Q){if(c!==j||A!==F){var w=J.push(n(J)+"scale(",null,",",null,")");Q.push({i:w-4,x:zf(c,j)},{i:w-2,x:zf(A,F)})}else if(j!==1||F!==1)J.push(n(J)+"scale("+j+","+F+")")}return function(c,A){var j=[],F=[];return c=u(c),A=u(A),i(c.translateX,c.translateY,A.translateX,A.translateY,j,F),y(c.rotate,A.rotate,j,F),t(c.skewX,A.skewX,j,F),_(c.scaleX,c.scaleY,A.scaleX,A.scaleY,j,F),c=A=null,function(J){var Q=-1,w=F.length,L;while(++Q=0)u._call.call(void 0,l);u=u._next}--d1}function mN(){Si=(Ec=U_.now())+Zc,d1=j_=0;try{MN()}finally{d1=0,jB(),Si=0}}function AB(){var u=U_.now(),l=u-Ec;if(l>PN)Zc-=l,Ec=u}function jB(){var u,l=Tc,f,r=1/0;while(l)if(l._call){if(r>l._time)r=l._time;u=l,l=l._next}else f=l._next,l._next=null,l=u?u._next=f:Tc=f;F_=u,F5(r)}function F5(u){if(d1)return;if(j_)j_=clearTimeout(j_);var l=u-Si;if(l>24){if(u<1/0)j_=setTimeout(mN,u-U_.now()-Zc);if(A_)A_=clearInterval(A_)}else{if(!A_)Ec=U_.now(),A_=setInterval(AB,PN);d1=1,CN(mN)}}function N_(u,l,f){var r=new J_;return l=l==null?0:+l,r.restart((n)=>{r.stop(),u(n+l)},l,f),r}var UB=Oi("start","end","cancel","interrupt"),JB=[],hN=0,RN=1,Bc=2,Hc=3,xN=4,Vc=5,q_=6;function M0(u,l,f,r,n,i){var y=u.__transition;if(!y)u.__transition={};else if(f in y)return;QB(u,f,{name:l,index:r,group:n,on:UB,tween:JB,time:i.time,delay:i.delay,duration:i.duration,ease:i.ease,timer:null,state:hN})}function W_(u,l){var f=al(u,l);if(f.state>hN)throw Error("too late; already scheduled");return f}function Ff(u,l){var f=al(u,l);if(f.state>Hc)throw Error("too late; already running");return f}function al(u,l){var f=u.__transition;if(!f||!(f=f[l]))throw Error("transition not found");return f}function QB(u,l,f){var r=u.__transition,n;r[l]=f,f.timer=Oc(i,0,f.time);function i(c){if(f.state=RN,f.timer.restart(y,f.delay,f.time),f.delay<=c)y(c-f.delay)}function y(c){var A,j,F,J;if(f.state!==RN)return _();for(A in r){if(J=r[A],J.name!==f.name)continue;if(J.state===Hc)return N_(y);if(J.state===xN)J.state=q_,J.timer.stop(),J.on.call("interrupt",u,u.__data__,J.index,J.group),delete r[A];else if(+ABc&&r.state=0)l=l.slice(0,f);return!l||l==="start"})}function pB(u,l,f){var r,n,i=YB(l)?W_:Ff;return function(){var y=i(this,u),t=y.on;if(t!==r)(n=(r=t).copy()).on(l,f);y.on=n}}function z5(u,l){var f=this._id;return arguments.length<2?al(this.node(),f).on.on(u):this.each(pB(f,u,l))}function mB(u){return function(){var l=this.parentNode;for(var f in this.__transition)if(+f!==u)return;if(l)l.removeChild(this)}}function T5(){return this.on("end.remove",mB(this._id))}function E5(u){var l=this._name,f=this._id;if(typeof u!=="function")u=Bn(u);for(var r=this._groups,n=r.length,i=Array(n),y=0;y()=>u;function p5(u,{sourceEvent:l,target:f,transform:r,dispatch:n}){Object.defineProperties(this,{type:{value:u,enumerable:!0,configurable:!0},sourceEvent:{value:l,enumerable:!0,configurable:!0},target:{value:f,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:n}})}function Ir(u,l,f){this.k=u,this.x=l,this.y=f}Ir.prototype={constructor:Ir,scale:function(u){return u===1?this:new Ir(this.k*u,this.x,this.y)},translate:function(u,l){return u===0&l===0?this:new Ir(this.k,this.x+this.k*u,this.y+this.k*l)},apply:function(u){return[u[0]*this.k+this.x,u[1]*this.k+this.y]},applyX:function(u){return u*this.k+this.x},applyY:function(u){return u*this.k+this.y},invert:function(u){return[(u[0]-this.x)/this.k,(u[1]-this.y)/this.k]},invertX:function(u){return(u-this.x)/this.k},invertY:function(u){return(u-this.y)/this.k},rescaleX:function(u){return u.copy().domain(u.range().map(this.invertX,this).map(u.invert,u))},rescaleY:function(u){return u.copy().domain(u.range().map(this.invertY,this).map(u.invert,u))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Yi=new Ir(1,0,0);K_.prototype=Ir.prototype;function K_(u){while(!u.__zoom)if(!(u=u.parentNode))return Yi;return u.__zoom}function xc(u){u.stopImmediatePropagation()}function pi(u){u.preventDefault(),u.stopImmediatePropagation()}function eB(u){return(!u.ctrlKey||u.type==="wheel")&&!u.button}function uV(){var u=this;if(u instanceof SVGElement){if(u=u.ownerSVGElement||u,u.hasAttribute("viewBox"))return u=u.viewBox.baseVal,[[u.x,u.y],[u.x+u.width,u.y+u.height]];return[[0,0],[u.width.baseVal.value,u.height.baseVal.value]]}return[[0,0],[u.clientWidth,u.clientHeight]]}function kN(){return this.__zoom||Yi}function lV(u){return-u.deltaY*(u.deltaMode===1?0.05:u.deltaMode?1:0.002)*(u.ctrlKey?10:1)}function fV(){return navigator.maxTouchPoints||"ontouchstart"in this}function rV(u,l,f){var r=u.invertX(l[0][0])-f[0][0],n=u.invertX(l[1][0])-f[1][0],i=u.invertY(l[0][1])-f[0][1],y=u.invertY(l[1][1])-f[1][1];return u.translate(n>r?(r+n)/2:Math.min(0,r)||Math.max(0,n),y>i?(i+y)/2:Math.min(0,i)||Math.max(0,y))}function G_(){var u=eB,l=uV,f=rV,r=lV,n=fV,i=[0,1/0],y=[[-1/0,-1/0],[1/0,1/0]],t=250,_=Xi,c=Oi("start","zoom","end"),A,j,F,J=500,Q=150,w=0,L=10;function U(O){O.property("__zoom",kN).on("wheel.zoom",E,{passive:!1}).on("mousedown.zoom",D).on("dblclick.zoom",h).filter(n).on("touchstart.zoom",V).on("touchmove.zoom",S).on("touchend.zoom touchcancel.zoom",p).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}U.transform=function(O,m,X,v){var T=O.selection?O.selection():O;if(T.property("__zoom",kN),O!==T)z(O,m,X,v);else T.interrupt().each(function(){Z(this,arguments).event(v).start().zoom(null,typeof m==="function"?m.apply(this,arguments):m).end()})},U.scaleBy=function(O,m,X,v){U.scaleTo(O,function(){var T=this.__zoom.k,Y=typeof m==="function"?m.apply(this,arguments):m;return T*Y},X,v)},U.scaleTo=function(O,m,X,v){U.transform(O,function(){var T=l.apply(this,arguments),Y=this.__zoom,k=X==null?W(T):typeof X==="function"?X.apply(this,arguments):X,I=Y.invert(k),b=typeof m==="function"?m.apply(this,arguments):m;return f(q(N(Y,b),k,I),T,y)},X,v)},U.translateBy=function(O,m,X,v){U.transform(O,function(){return f(this.__zoom.translate(typeof m==="function"?m.apply(this,arguments):m,typeof X==="function"?X.apply(this,arguments):X),l.apply(this,arguments),y)},null,v)},U.translateTo=function(O,m,X,v,T){U.transform(O,function(){var Y=l.apply(this,arguments),k=this.__zoom,I=v==null?W(Y):typeof v==="function"?v.apply(this,arguments):v;return f(Yi.translate(I[0],I[1]).scale(k.k).translate(typeof m==="function"?-m.apply(this,arguments):-m,typeof X==="function"?-X.apply(this,arguments):-X),Y,y)},v,T)};function N(O,m){return m=Math.max(i[0],Math.min(i[1],m)),m===O.k?O:new Ir(m,O.x,O.y)}function q(O,m,X){var v=m[0]-X[0]*O.k,T=m[1]-X[1]*O.k;return v===O.x&&T===O.y?O:new Ir(O.k,v,T)}function W(O){return[(+O[0][0]+ +O[1][0])/2,(+O[0][1]+ +O[1][1])/2]}function z(O,m,X,v){O.on("start.zoom",function(){Z(this,arguments).event(v).start()}).on("interrupt.zoom end.zoom",function(){Z(this,arguments).event(v).end()}).tween("zoom",function(){var T=this,Y=arguments,k=Z(T,Y).event(v),I=l.apply(T,Y),b=X==null?W(I):typeof X==="function"?X.apply(T,Y):X,o=Math.max(I[1][0]-I[0][0],I[1][1]-I[0][1]),g=T.__zoom,x=typeof m==="function"?m.apply(T,Y):m,lu=_(g.invert(b).concat(o/g.k),x.invert(b).concat(o/x.k));return function(_u){if(_u===1)_u=x;else{var $u=lu(_u),ju=o/$u[2];_u=new Ir(ju,b[0]-$u[0]*ju,b[1]-$u[1]*ju)}k.zoom(null,_u)}})}function Z(O,m,X){return!X&&O.__zooming||new H(O,m)}function H(O,m){this.that=O,this.args=m,this.active=0,this.sourceEvent=null,this.extent=l.apply(O,m),this.taps=0}H.prototype={event:function(O){if(O)this.sourceEvent=O;return this},start:function(){if(++this.active===1)this.that.__zooming=this,this.emit("start");return this},zoom:function(O,m){if(this.mouse&&O!=="mouse")this.mouse[1]=m.invert(this.mouse[0]);if(this.touch0&&O!=="touch")this.touch0[1]=m.invert(this.touch0[0]);if(this.touch1&&O!=="touch")this.touch1[1]=m.invert(this.touch1[0]);return this.that.__zoom=m,this.emit("zoom"),this},end:function(){if(--this.active===0)delete this.that.__zooming,this.emit("end");return this},emit:function(O){var m=sl(this.that).datum();c.call(O,this.that,new p5(O,{sourceEvent:this.sourceEvent,target:U,type:O,transform:this.that.__zoom,dispatch:c}),m)}};function E(O,...m){if(!u.apply(this,arguments))return;var X=Z(this,m).event(O),v=this.__zoom,T=Math.max(i[0],Math.min(i[1],v.k*Math.pow(2,r.apply(this,arguments)))),Y=Gf(O);if(X.wheel){if(X.mouse[0][0]!==Y[0]||X.mouse[0][1]!==Y[1])X.mouse[1]=v.invert(X.mouse[0]=Y);clearTimeout(X.wheel)}else if(v.k===T)return;else X.mouse=[Y,v.invert(Y)],Dn(this),X.start();pi(O),X.wheel=setTimeout(k,Q),X.zoom("mouse",f(q(N(v,T),X.mouse[0],X.mouse[1]),X.extent,y));function k(){X.wheel=null,X.end()}}function D(O,...m){if(F||!u.apply(this,arguments))return;var X=O.currentTarget,v=Z(this,m,!0).event(O),T=sl(O.view).on("mousemove.zoom",b,!0).on("mouseup.zoom",o,!0),Y=Gf(O,X),k=O.clientX,I=O.clientY;g1(O.view),xc(O),v.mouse=[Y,this.__zoom.invert(Y)],Dn(this),v.start();function b(g){if(pi(g),!v.moved){var x=g.clientX-k,lu=g.clientY-I;v.moved=x*x+lu*lu>w}v.event(g).zoom("mouse",f(q(v.that.__zoom,v.mouse[0]=Gf(g,X),v.mouse[1]),v.extent,y))}function o(g){T.on("mousemove.zoom mouseup.zoom",null),f_(g.view,v.moved),pi(g),v.event(g).end()}}function h(O,...m){if(!u.apply(this,arguments))return;var X=this.__zoom,v=Gf(O.changedTouches?O.changedTouches[0]:O,this),T=X.invert(v),Y=X.k*(O.shiftKey?0.5:2),k=f(q(N(X,Y),v,T),l.apply(this,m),y);if(pi(O),t>0)sl(this).transition().duration(t).call(z,k,v,O);else sl(this).call(U.transform,k,v,O)}function V(O,...m){if(!u.apply(this,arguments))return;var X=O.touches,v=X.length,T=Z(this,m,O.changedTouches.length===v).event(O),Y,k,I,b;xc(O);for(k=0;k"[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",error002:()=>"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",error003:(u)=>`Node type "${u}" not found. Using fallback type "default".`,error004:()=>"The React Flow parent container needs a width and a height to render the graph.",error005:()=>"Only child nodes can use a parent extent.",error006:()=>"Can't create edge. An edge needs a source and a target.",error007:(u)=>`The old edge with id=${u} does not exist.`,error009:(u)=>`Marker type "${u}" doesn't exist.`,error008:(u,{id:l,sourceHandle:f,targetHandle:r})=>`Couldn't create edge for ${u} handle id: "${u==="source"?f:r}", edge id: ${l}.`,error010:()=>"Handle: No node id found. Make sure to only use a Handle inside a custom Node.",error011:(u)=>`Edge type "${u}" not found. Using fallback type "default".`,error012:(u)=>`Node with id "${u}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,error013:(u="react")=>`It seems that you haven't loaded the styles. Please import '@xyflow/${u}/dist/style.css' or base.css to make sure everything is working properly.`,error014:()=>"useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",error015:()=>"It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."},ny=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]],R5=["Enter"," ","Escape"],x5={"node.a11yDescription.default":"Press enter or space to select a node. Press delete to remove it and escape to cancel.","node.a11yDescription.keyboardDisabled":"Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.","node.a11yDescription.ariaLiveMessage":({direction:u,x:l,y:f})=>`Moved selected node ${u}. New position, x: ${l}, y: ${f}`,"edge.a11yDescription.default":"Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.","controls.ariaLabel":"Control Panel","controls.zoomIn.ariaLabel":"Zoom In","controls.zoomOut.ariaLabel":"Zoom Out","controls.fitView.ariaLabel":"Fit View","controls.interactive.ariaLabel":"Toggle Interactivity","minimap.ariaLabel":"Mini Map","handle.ariaLabel":"Handle"},Yn;(function(u){u.Strict="strict",u.Loose="loose"})(Yn||(Yn={}));var x0;(function(u){u.Free="free",u.Vertical="vertical",u.Horizontal="horizontal"})(x0||(x0={}));var mi;(function(u){u.Partial="partial",u.Full="full"})(mi||(mi={}));var h5={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null,pointer:null},c0;(function(u){u.Bezier="default",u.Straight="straight",u.Step="step",u.SmoothStep="smoothstep",u.SimpleBezier="simplebezier"})(c0||(c0={}));var pn;(function(u){u.Arrow="arrow",u.ArrowClosed="arrowclosed"})(pn||(pn={}));var Lu;(function(u){u.Left="left",u.Top="top",u.Right="right",u.Bottom="bottom"})(Lu||(Lu={}));var IN={[Lu.Left]:Lu.Right,[Lu.Right]:Lu.Left,[Lu.Top]:Lu.Bottom,[Lu.Bottom]:Lu.Top};function b5(u){return u===null?null:u?"valid":"invalid"}var v5=(u)=>("id"in u)&&("source"in u)&&("target"in u),yq=(u)=>("id"in u)&&("position"in u)&&!("source"in u)&&!("target"in u),k5=(u)=>("id"in u)&&("internals"in u)&&!("source"in u)&&!("target"in u);var E_=(u,l=[0,0])=>{let{width:f,height:r}=A0(u),n=u.origin??l,i=f*n[0],y=r*n[1];return{x:u.position.x-i,y:u.position.y-y}},I5=(u,l={nodeOrigin:[0,0]})=>{if(u.length===0)return{x:0,y:0,width:0,height:0};let f=u.reduce((r,n)=>{let i=typeof n==="string",y=!l.nodeLookup&&!i?n:void 0;if(l.nodeLookup)y=i?l.nodeLookup.get(n):!k5(n)?l.nodeLookup.get(n.id):n;let t=y?vc(y,l.nodeOrigin):{x:0,y:0,x2:0,y2:0};return Ic(r,t)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return gc(f)},iy=(u,l={})=>{let f={x:1/0,y:1/0,x2:-1/0,y2:-1/0},r=!1;return u.forEach((n)=>{if(l.filter===void 0||l.filter(n))f=Ic(f,vc(n)),r=!0}),r?gc(f):{x:0,y:0,width:0,height:0}},kc=(u,l,[f,r,n]=[0,0,1],i=!1,y=!1)=>{let t={..._y(l,[f,r,n]),width:l.width/n,height:l.height/n},_=[];for(let c of u.values()){let{measured:A,selectable:j=!0,hidden:F=!1}=c;if(y&&!j||F)continue;let J=A.width??c.width??c.initialWidth??null,Q=A.height??c.height??c.initialHeight??null,w=yy(t,Ci(c)),L=(J??0)*(Q??0),U=i&&w>0;if(!c.internals.handleBounds||U||w>=L||c.dragging)_.push(c)}return _},tq=(u,l)=>{let f=new Set;return u.forEach((r)=>{f.add(r.id)}),l.filter((r)=>f.has(r.source)||f.has(r.target))};function nV(u,l){let f=new Map,r=l?.nodes?new Set(l.nodes.map((n)=>n.id)):null;return u.forEach((n)=>{if(n.measured.width&&n.measured.height&&(l?.includeHiddenNodes||!n.hidden)&&(!r||r.has(n.id)))f.set(n.id,n)}),f}async function _q({nodes:u,width:l,height:f,panZoom:r,minZoom:n,maxZoom:i},y){if(u.size===0)return Promise.resolve(!0);let t=nV(u,y),_=iy(t),c=Z_(_,l,f,y?.minZoom??n,y?.maxZoom??i,y?.padding??0.1);return await r.setViewport(c,{duration:y?.duration,ease:y?.ease,interpolate:y?.interpolate}),Promise.resolve(!0)}function g5({nodeId:u,nextPosition:l,nodeLookup:f,nodeOrigin:r=[0,0],nodeExtent:n,onError:i}){let y=f.get(u),t=y.parentId?f.get(y.parentId):void 0,{x:_,y:c}=t?t.internals.positionAbsolute:{x:0,y:0},A=y.origin??r,j=y.extent||n;if(y.extent==="parent"&&!y.expandParent)if(!t)i?.("005",Ur.error005());else{let J=t.measured.width,Q=t.measured.height;if(J&&Q)j=[[_,c],[_+J,c+Q]]}else if(t&&ry(y.extent))j=[[y.extent[0][0]+_,y.extent[0][1]+c],[y.extent[1][0]+_,y.extent[1][1]+c]];let F=ry(j)?Pi(l,j,y.measured):l;if(y.measured.width===void 0||y.measured.height===void 0)i?.("015",Ur.error015());return{position:{x:F.x-_+(y.measured.width??0)*A[0],y:F.y-c+(y.measured.height??0)*A[1]},positionAbsolute:F}}async function $q({nodesToRemove:u=[],edgesToRemove:l=[],nodes:f,edges:r,onBeforeDelete:n}){let i=new Set(u.map((F)=>F.id)),y=[];for(let F of f){if(F.deletable===!1)continue;let J=i.has(F.id),Q=!J&&F.parentId&&y.find((w)=>w.id===F.parentId);if(J||Q)y.push(F)}let t=new Set(l.map((F)=>F.id)),_=r.filter((F)=>F.deletable!==!1),A=tq(y,_);for(let F of _)if(t.has(F.id)&&!A.find((Q)=>Q.id===F.id))A.push(F);if(!n)return{edges:A,nodes:y};let j=await n({nodes:y,edges:A});if(typeof j==="boolean")return j?{edges:A,nodes:y}:{edges:[],nodes:[]};return j}var fy=(u,l=0,f=1)=>Math.min(Math.max(u,l),f),Pi=(u={x:0,y:0},l,f)=>({x:fy(u.x,l[0][0],l[1][0]-(f?.width??0)),y:fy(u.y,l[0][1],l[1][1]-(f?.height??0))});function cq(u,l,f){let{width:r,height:n}=A0(f),{x:i,y}=f.internals.positionAbsolute;return Pi(u,[[i,y],[i+r,y+n]],l)}var gN=(u,l,f)=>{if(uf)return-fy(Math.abs(u-f),1,l)/l;return 0},Aq=(u,l,f=15,r=40)=>{let n=gN(u.x,r,l.width-r)*f,i=gN(u.y,r,l.height-r)*f;return[n,i]},Ic=(u,l)=>({x:Math.min(u.x,l.x),y:Math.min(u.y,l.y),x2:Math.max(u.x2,l.x2),y2:Math.max(u.y2,l.y2)}),M5=({x:u,y:l,width:f,height:r})=>({x:u,y:l,x2:u+f,y2:l+r}),gc=({x:u,y:l,x2:f,y2:r})=>({x:u,y:l,width:f-u,height:r-l}),Ci=(u,l=[0,0])=>{let{x:f,y:r}=k5(u)?u.internals.positionAbsolute:E_(u,l);return{x:f,y:r,width:u.measured?.width??u.width??u.initialWidth??0,height:u.measured?.height??u.height??u.initialHeight??0}},vc=(u,l=[0,0])=>{let{x:f,y:r}=k5(u)?u.internals.positionAbsolute:E_(u,l);return{x:f,y:r,x2:f+(u.measured?.width??u.width??u.initialWidth??0),y2:r+(u.measured?.height??u.height??u.initialHeight??0)}},s5=(u,l)=>gc(Ic(M5(u),M5(l))),yy=(u,l)=>{let f=Math.max(0,Math.min(u.x+u.width,l.x+l.width)-Math.max(u.x,l.x)),r=Math.max(0,Math.min(u.y+u.height,l.y+l.height)-Math.max(u.y,l.y));return Math.ceil(f*r)},a5=(u)=>Zr(u.width)&&Zr(u.height)&&Zr(u.x)&&Zr(u.y),Zr=(u)=>!isNaN(u)&&isFinite(u),o5=(u,l)=>{},ty=(u,l=[1,1])=>{return{x:l[0]*Math.round(u.x/l[0]),y:l[1]*Math.round(u.y/l[1])}},_y=({x:u,y:l},[f,r,n],i=!1,y=[1,1])=>{let t={x:(u-f)/n,y:(l-r)/n};return i?ty(t,y):t},T_=({x:u,y:l},[f,r,n])=>{return{x:u*n+f,y:l*n+r}};function uy(u,l){if(typeof u==="number")return Math.floor((l-l/(1+u))*0.5);if(typeof u==="string"&&u.endsWith("px")){let f=parseFloat(u);if(!Number.isNaN(f))return Math.floor(f)}if(typeof u==="string"&&u.endsWith("%")){let f=parseFloat(u);if(!Number.isNaN(f))return Math.floor(l*f*0.01)}return console.error(`[React Flow] The padding value "${u}" is invalid. Please provide a number or a string with a valid unit (px or %).`),0}function iV(u,l,f){if(typeof u==="string"||typeof u==="number"){let r=uy(u,f),n=uy(u,l);return{top:r,right:n,bottom:r,left:n,x:n*2,y:r*2}}if(typeof u==="object"){let r=uy(u.top??u.y??0,f),n=uy(u.bottom??u.y??0,f),i=uy(u.left??u.x??0,l),y=uy(u.right??u.x??0,l);return{top:r,right:y,bottom:n,left:i,x:i+y,y:r+n}}return{top:0,right:0,bottom:0,left:0,x:0,y:0}}function yV(u,l,f,r,n,i){let{x:y,y:t}=T_(u,[l,f,r]),{x:_,y:c}=T_({x:u.x+u.width,y:u.y+u.height},[l,f,r]),A=n-_,j=i-c;return{left:Math.floor(y),top:Math.floor(t),right:Math.floor(A),bottom:Math.floor(j)}}var Z_=(u,l,f,r,n,i)=>{let y=iV(i,l,f),t=(l-y.x)/u.width,_=(f-y.y)/u.height,c=Math.min(t,_),A=fy(c,r,n),j=u.x+u.width/2,F=u.y+u.height/2,J=l/2-j*A,Q=f/2-F*A,w=yV(u,J,Q,A,l,f),L={left:Math.min(w.left-y.left,0),top:Math.min(w.top-y.top,0),right:Math.min(w.right-y.right,0),bottom:Math.min(w.bottom-y.bottom,0)};return{x:J-L.left+L.right,y:Q-L.top+L.bottom,zoom:A}},$y=()=>typeof navigator<"u"&&navigator?.userAgent?.indexOf("Mac")>=0;function ry(u){return u!==void 0&&u!==null&&u!=="parent"}function A0(u){return{width:u.measured?.width??u.width??u.initialWidth??0,height:u.measured?.height??u.height??u.initialHeight??0}}function d5(u){return(u.measured?.width??u.width??u.initialWidth)!==void 0&&(u.measured?.height??u.height??u.initialHeight)!==void 0}function e5(u,l={width:0,height:0},f,r,n){let i={...u},y=r.get(f);if(y){let t=y.origin||n;i.x+=y.internals.positionAbsolute.x-(l.width??0)*t[0],i.y+=y.internals.positionAbsolute.y-(l.height??0)*t[1]}return i}function u9(u,l){if(u.size!==l.size)return!1;for(let f of u)if(!l.has(f))return!1;return!0}function jq(){let u,l;return{promise:new Promise((r,n)=>{u=r,l=n}),resolve:u,reject:l}}function Fq(u){return{...x5,...u||{}}}function z_(u,{snapGrid:l=[0,0],snapToGrid:f=!1,transform:r,containerBounds:n}){let{x:i,y}=Or(u),t=_y({x:i-(n?.left??0),y:y-(n?.top??0)},r),{x:_,y:c}=f?ty(t,l):t;return{xSnapped:_,ySnapped:c,...t}}var sc=(u)=>({width:u.offsetWidth,height:u.offsetHeight}),l9=(u)=>u?.getRootNode?.()||window?.document,tV=["INPUT","SELECT","TEXTAREA"];function f9(u){let l=u.composedPath?.()?.[0]||u.target;if(l?.nodeType!==1)return!1;return tV.includes(l.nodeName)||l.hasAttribute("contenteditable")||!!l.closest(".nokey")}var r9=(u)=>("clientX"in u),Or=(u,l)=>{let f=r9(u),r=f?u.clientX:u.touches?.[0].clientX,n=f?u.clientY:u.touches?.[0].clientY;return{x:r-(l?.left??0),y:n-(l?.top??0)}},sN=(u,l,f,r,n)=>{let i=l.querySelectorAll(`.${u}`);if(!i||!i.length)return null;return Array.from(i).map((y)=>{let t=y.getBoundingClientRect();return{id:y.getAttribute("data-handleid"),type:u,nodeId:n,position:y.getAttribute("data-handlepos"),x:(t.left-f.left)/r,y:(t.top-f.top)/r,...sc(y)}})};function ac({sourceX:u,sourceY:l,targetX:f,targetY:r,sourceControlX:n,sourceControlY:i,targetControlX:y,targetControlY:t}){let _=u*0.125+n*0.375+y*0.375+f*0.125,c=l*0.125+i*0.375+t*0.375+r*0.125,A=Math.abs(_-u),j=Math.abs(c-l);return[_,c,A,j]}function hc(u,l){if(u>=0)return 0.5*u;return l*25*Math.sqrt(-u)}function aN({pos:u,x1:l,y1:f,x2:r,y2:n,c:i}){switch(u){case Lu.Left:return[l-hc(l-r,i),f];case Lu.Right:return[l+hc(r-l,i),f];case Lu.Top:return[l,f-hc(f-n,i)];case Lu.Bottom:return[l,f+hc(n-f,i)]}}function oc({sourceX:u,sourceY:l,sourcePosition:f=Lu.Bottom,targetX:r,targetY:n,targetPosition:i=Lu.Top,curvature:y=0.25}){let[t,_]=aN({pos:f,x1:u,y1:l,x2:r,y2:n,c:y}),[c,A]=aN({pos:i,x1:r,y1:n,x2:u,y2:l,c:y}),[j,F,J,Q]=ac({sourceX:u,sourceY:l,targetX:r,targetY:n,sourceControlX:t,sourceControlY:_,targetControlX:c,targetControlY:A});return[`M${u},${l} C${t},${_} ${c},${A} ${r},${n}`,j,F,J,Q]}function n9({sourceX:u,sourceY:l,targetX:f,targetY:r}){let n=Math.abs(f-u)/2,i=f0}var _V=({source:u,sourceHandle:l,target:f,targetHandle:r})=>`xy-edge__${u}${l||""}-${f}${r||""}`,$V=(u,l)=>{return l.some((f)=>f.source===u.source&&f.target===u.target&&(f.sourceHandle===u.sourceHandle||!f.sourceHandle&&!u.sourceHandle)&&(f.targetHandle===u.targetHandle||!f.targetHandle&&!u.targetHandle))},i9=(u,l,f={})=>{if(!u.source||!u.target)return o5("006",Ur.error006()),l;let r=f.getEdgeId||_V,n;if(v5(u))n={...u};else n={...u,id:r(u)};if($V(n,l))return l;if(n.sourceHandle===null)delete n.sourceHandle;if(n.targetHandle===null)delete n.targetHandle;return l.concat(n)};function dc({sourceX:u,sourceY:l,targetX:f,targetY:r}){let[n,i,y,t]=n9({sourceX:u,sourceY:l,targetX:f,targetY:r});return[`M ${u},${l}L ${f},${r}`,n,i,y,t]}var oN={[Lu.Left]:{x:-1,y:0},[Lu.Right]:{x:1,y:0},[Lu.Top]:{x:0,y:-1},[Lu.Bottom]:{x:0,y:1}},cV=({source:u,sourcePosition:l=Lu.Bottom,target:f})=>{if(l===Lu.Left||l===Lu.Right)return u.xMath.sqrt(Math.pow(l.x-u.x,2)+Math.pow(l.y-u.y,2));function AV({source:u,sourcePosition:l=Lu.Bottom,target:f,targetPosition:r=Lu.Top,center:n,offset:i,stepPosition:y}){let t=oN[l],_=oN[r],c={x:u.x+t.x*i,y:u.y+t.y*i},A={x:f.x+_.x*i,y:f.y+_.y*i},j=cV({source:c,sourcePosition:l,target:A}),F=j.x!==0?"x":"y",J=j[F],Q=[],w,L,U={x:0,y:0},N={x:0,y:0},[,,q,W]=n9({sourceX:u.x,sourceY:u.y,targetX:f.x,targetY:f.y});if(t[F]*_[F]===-1){if(F==="x")w=n.x??c.x+(A.x-c.x)*y,L=n.y??(c.y+A.y)/2;else w=n.x??(c.x+A.x)/2,L=n.y??c.y+(A.y-c.y)*y;let E=[{x:w,y:c.y},{x:w,y:A.y}],D=[{x:c.x,y:L},{x:A.x,y:L}];if(t[F]===J)Q=F==="x"?E:D;else Q=F==="x"?D:E}else{let E=[{x:c.x,y:A.y}],D=[{x:A.x,y:c.y}];if(F==="x")Q=t.x===J?D:E;else Q=t.y===J?E:D;if(l===r){let O=Math.abs(u[F]-f[F]);if(O<=i){let m=Math.min(i-1,i-O);if(t[F]===J)U[F]=(c[F]>u[F]?-1:1)*m;else N[F]=(A[F]>f[F]?-1:1)*m}}if(l!==r){let O=F==="x"?"y":"x",m=t[F]===_[O],X=c[O]>A[O],v=c[O]=p)w=(h.x+V.x)/2,L=Q[0].y;else w=Q[0].x,L=(h.y+V.y)/2}let z={x:c.x+U.x,y:c.y+U.y},Z={x:A.x+N.x,y:A.y+N.y};return[[u,...z.x!==Q[0].x||z.y!==Q[0].y?[z]:[],...Q,...Z.x!==Q[Q.length-1].x||Z.y!==Q[Q.length-1].y?[Z]:[],f],w,L,q,W]}function jV(u,l,f,r){let n=Math.min(dN(u,l)/2,dN(l,f)/2,r),{x:i,y}=l;if(u.x===i&&i===f.x||u.y===y&&y===f.y)return`L${i} ${y}`;if(u.y===y){let c=u.xf.id===l))||null}function ec(u,l){if(!u)return"";if(typeof u==="string")return u;return`${l?`${l}__`:""}${Object.keys(u).sort().map((r)=>`${r}=${u[r]}`).join("&")}`}function Nq(u,{id:l,defaultColor:f,defaultMarkerStart:r,defaultMarkerEnd:n}){let i=new Set;return u.reduce((y,t)=>{return[t.markerStart||r,t.markerEnd||n].forEach((_)=>{if(_&&typeof _==="object"){let c=ec(_,l);if(!i.has(c))y.push({id:c,color:_.color||f,..._}),i.add(c)}}),y},[]).sort((y,t)=>y.id.localeCompare(t.id))}var qq=1000,FV=10,y9={nodeOrigin:[0,0],nodeExtent:ny,elevateNodesOnSelect:!0,zIndexMode:"basic",defaults:{}},UV={...y9,checkEquality:!0};function t9(u,l){let f={...u};for(let r in l)if(l[r]!==void 0)f[r]=l[r];return f}function Wq(u,l,f){let r=t9(y9,f);for(let n of u.values())if(n.parentId)$9(n,u,l,r);else{let i=E_(n,r.nodeOrigin),y=ry(n.extent)?n.extent:r.nodeExtent,t=Pi(i,y,A0(n));n.internals.positionAbsolute=t}}function JV(u,l){if(!u.handles)return!u.measured?void 0:l?.internals.handleBounds;let f=[],r=[];for(let n of u.handles){let i={id:n.id,width:n.width??1,height:n.height??1,nodeId:u.id,x:n.x,y:n.y,position:n.position,type:n.type};if(n.type==="source")f.push(i);else if(n.type==="target")r.push(i)}return{source:f,target:r}}function _9(u){return u==="manual"}function u6(u,l,f,r={}){let n=t9(UV,r),i={i:0},y=new Map(l),t=n?.elevateNodesOnSelect&&!_9(n.zIndexMode)?qq:0,_=u.length>0,c=!1;l.clear(),f.clear();for(let A of u){let j=y.get(A.id);if(n.checkEquality&&A===j?.internals.userNode)l.set(A.id,j);else{let F=E_(A,n.nodeOrigin),J=ry(A.extent)?A.extent:n.nodeExtent,Q=Pi(F,J,A0(A));j={...n.defaults,...A,measured:{width:A.measured?.width,height:A.measured?.height},internals:{positionAbsolute:Q,handleBounds:JV(A,j),z:wq(A,t,n.zIndexMode),userNode:A}},l.set(A.id,j)}if((j.measured===void 0||j.measured.width===void 0||j.measured.height===void 0)&&!j.hidden)_=!1;if(A.parentId)$9(j,l,f,r,i);c||=A.selected??!1}return{nodesInitialized:_,hasSelectedNodes:c}}function QV(u,l){if(!u.parentId)return;let f=l.get(u.parentId);if(f)f.set(u.id,u);else l.set(u.parentId,new Map([[u.id,u]]))}function $9(u,l,f,r,n){let{elevateNodesOnSelect:i,nodeOrigin:y,nodeExtent:t,zIndexMode:_}=t9(y9,r),c=u.parentId,A=l.get(c);if(!A){console.warn(`Parent node ${c} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}if(QV(u,f),n&&!A.parentId&&A.internals.rootParentIndex===void 0&&_==="auto")A.internals.rootParentIndex=++n.i,A.internals.z=A.internals.z+n.i*FV;if(n&&A.internals.rootParentIndex!==void 0)n.i=A.internals.rootParentIndex;let j=i&&!_9(_)?qq:0,{x:F,y:J,z:Q}=NV(u,A,y,t,j,_),{positionAbsolute:w}=u.internals,L=F!==w.x||J!==w.y;if(L||Q!==u.internals.z)l.set(u.id,{...u,internals:{...u.internals,positionAbsolute:L?{x:F,y:J}:w,z:Q}})}function wq(u,l,f){let r=Zr(u.zIndex)?u.zIndex:0;if(_9(f))return r;return r+(u.selected?l:0)}function NV(u,l,f,r,n,i){let{x:y,y:t}=l.internals.positionAbsolute,_=A0(u),c=E_(u,f),A=ry(u.extent)?Pi(c,u.extent,_):c,j=Pi({x:y+A.x,y:t+A.y},r,_);if(u.extent==="parent")j=cq(j,_,l);let F=wq(u,n,i),J=l.internals.z??0;return{x:j.x,y:j.y,z:J>=F?J+1:F}}function l6(u,l,f,r=[0,0]){let n=[],i=new Map;for(let y of u){let t=l.get(y.parentId);if(!t)continue;let _=i.get(y.parentId)?.expandedRect??Ci(t),c=s5(_,y.rect);i.set(y.parentId,{expandedRect:c,parent:t})}if(i.size>0)i.forEach(({expandedRect:y,parent:t},_)=>{let c=t.internals.positionAbsolute,A=A0(t),j=t.origin??r,F=y.x0||J>0||L||U)n.push({id:_,type:"position",position:{x:t.position.x-F+L,y:t.position.y-J+U}}),f.get(_)?.forEach((N)=>{if(!u.some((q)=>q.id===N.id))n.push({id:N.id,type:"position",position:{x:N.position.x+F,y:N.position.y+J}})});if(A.width0){let J=l6(F,l,f,n);c.push(...J)}return{changes:c,updatedInternals:_}}async function Kq({delta:u,panZoom:l,transform:f,translateExtent:r,width:n,height:i}){if(!l||!u.x&&!u.y)return Promise.resolve(!1);let y=await l.setViewportConstrained({x:f[0]+u.x,y:f[1]+u.y,zoom:f[2]},[[0,0],[n,i]],r),t=!!y&&(y.x!==f[0]||y.y!==f[1]||y.k!==f[2]);return Promise.resolve(t)}function fq(u,l,f,r,n,i){let y=n,t=r.get(y)||new Map;r.set(y,t.set(f,l)),y=`${n}-${u}`;let _=r.get(y)||new Map;if(r.set(y,_.set(f,l)),i){y=`${n}-${u}-${i}`;let c=r.get(y)||new Map;r.set(y,c.set(f,l))}}function c9(u,l,f){u.clear(),l.clear();for(let r of f){let{source:n,target:i,sourceHandle:y=null,targetHandle:t=null}=r,_={edgeId:r.id,source:n,target:i,sourceHandle:y,targetHandle:t},c=`${n}-${y}--${i}-${t}`,A=`${i}-${t}--${n}-${y}`;fq("source",_,A,u,n,y),fq("target",_,c,u,i,t),l.set(r.id,r)}}function Gq(u,l){if(!u.parentId)return!1;let f=l.get(u.parentId);if(!f)return!1;if(f.selected)return!0;return Gq(f,l)}function rq(u,l,f){let r=u;do{if(r?.matches?.(l))return!0;if(r===f)return!1;r=r?.parentElement}while(r);return!1}function qV(u,l,f,r){let n=new Map;for(let[i,y]of u)if((y.selected||y.id===r)&&(!y.parentId||!Gq(y,u))&&(y.draggable||l&&typeof y.draggable>"u")){let t=u.get(i);if(t)n.set(i,{id:i,position:t.position||{x:0,y:0},distance:{x:f.x-t.internals.positionAbsolute.x,y:f.y-t.internals.positionAbsolute.y},extent:t.extent,parentId:t.parentId,origin:t.origin,expandParent:t.expandParent,internals:{positionAbsolute:t.internals.positionAbsolute||{x:0,y:0}},measured:{width:t.measured.width??0,height:t.measured.height??0}})}return n}function m5({nodeId:u,dragItems:l,nodeLookup:f,dragging:r=!0}){let n=[];for(let[y,t]of l){let _=f.get(y)?.internals.userNode;if(_)n.push({..._,position:t.position,dragging:r})}if(!u)return[n[0],n];let i=f.get(u)?.internals.userNode;return[!i?n[0]:{...i,position:l.get(u)?.position||i.position,dragging:r},n]}function WV({dragItems:u,snapGrid:l,x:f,y:r}){let n=u.values().next().value;if(!n)return null;let i={x:f-n.distance.x,y:r-n.distance.y},y=ty(i,l);return{x:y.x-i.x,y:y.y-i.y}}function zq({onNodeMouseDown:u,getStoreItems:l,onDragStart:f,onDrag:r,onDragStop:n}){let i={x:null,y:null},y=0,t=new Map,_=!1,c={x:0,y:0},A=null,j=!1,F=null,J=!1,Q=!1,w=null;function L({noDragClassName:N,handleSelector:q,domNode:W,isSelectable:z,nodeId:Z,nodeClickDistance:H=0}){F=sl(W);function E({x:S,y:p}){let{nodeLookup:O,nodeExtent:m,snapGrid:X,snapToGrid:v,nodeOrigin:T,onNodeDrag:Y,onSelectionDrag:k,onError:I,updateNodePositions:b}=l();i={x:S,y:p};let o=!1,g=t.size>1,x=g&&m?M5(iy(t)):null,lu=g&&v?WV({dragItems:t,snapGrid:X,x:S,y:p}):null;for(let[_u,$u]of t){if(!O.has(_u))continue;let ju={x:S-$u.distance.x,y:p-$u.distance.y};if(v)ju=lu?{x:Math.round(ju.x+lu.x),y:Math.round(ju.y+lu.y)}:ty(ju,X);let zu=null;if(g&&m&&!$u.extent&&x){let{positionAbsolute:e}=$u.internals,uu=e.x-x.x+m[0][0],Ku=e.x+$u.measured.width-x.x2+m[1][0],s=e.y-x.y+m[0][1],Nu=e.y+$u.measured.height-x.y2+m[1][1];zu=[[uu,s],[Ku,Nu]]}let{position:Wu,positionAbsolute:P}=g5({nodeId:_u,nextPosition:ju,nodeLookup:O,nodeExtent:zu?zu:m,nodeOrigin:T,onError:I});o=o||$u.position.x!==Wu.x||$u.position.y!==Wu.y,$u.position=Wu,$u.internals.positionAbsolute=P}if(Q=Q||o,!o)return;if(b(t,!0),w&&(r||Y||!Z&&k)){let[_u,$u]=m5({nodeId:Z,dragItems:t,nodeLookup:O});if(r?.(w,t,_u,$u),Y?.(w,_u,$u),!Z)k?.(w,$u)}}async function D(){if(!A)return;let{transform:S,panBy:p,autoPanSpeed:O,autoPanOnNodeDrag:m}=l();if(!m){_=!1,cancelAnimationFrame(y);return}let[X,v]=Aq(c,A,O);if(X!==0||v!==0){if(i.x=(i.x??0)-X/S[2],i.y=(i.y??0)-v/S[2],await p({x:X,y:v}))E(i)}y=requestAnimationFrame(D)}function h(S){let{nodeLookup:p,multiSelectionActive:O,nodesDraggable:m,transform:X,snapGrid:v,snapToGrid:T,selectNodesOnDrag:Y,onNodeDragStart:k,onSelectionDragStart:I,unselectNodesAndEdges:b}=l();if(j=!0,(!Y||!z)&&!O&&Z){if(!p.get(Z)?.selected)b()}if(z&&Y&&Z)u?.(Z);let o=z_(S.sourceEvent,{transform:X,snapGrid:v,snapToGrid:T,containerBounds:A});if(i=o,t=qV(p,m,o,Z),t.size>0&&(f||k||!Z&&I)){let[g,x]=m5({nodeId:Z,dragItems:t,nodeLookup:p});if(f?.(S.sourceEvent,t,g,x),k?.(S.sourceEvent,g,x),!Z)I?.(S.sourceEvent,x)}}let V=i_().clickDistance(H).on("start",(S)=>{let{domNode:p,nodeDragThreshold:O,transform:m,snapGrid:X,snapToGrid:v}=l();if(A=p?.getBoundingClientRect()||null,J=!1,Q=!1,w=S.sourceEvent,O===0)h(S);i=z_(S.sourceEvent,{transform:m,snapGrid:X,snapToGrid:v,containerBounds:A}),c=Or(S.sourceEvent,A)}).on("drag",(S)=>{let{autoPanOnNodeDrag:p,transform:O,snapGrid:m,snapToGrid:X,nodeDragThreshold:v,nodeLookup:T}=l(),Y=z_(S.sourceEvent,{transform:O,snapGrid:m,snapToGrid:X,containerBounds:A});if(w=S.sourceEvent,S.sourceEvent.type==="touchmove"&&S.sourceEvent.touches.length>1||Z&&!T.has(Z))J=!0;if(J)return;if(!_&&p&&j)_=!0,D();if(!j){let k=Or(S.sourceEvent,A),I=k.x-c.x,b=k.y-c.y;if(Math.sqrt(I*I+b*b)>v)h(S)}if((i.x!==Y.xSnapped||i.y!==Y.ySnapped)&&t&&j)c=Or(S.sourceEvent,A),E(Y)}).on("end",(S)=>{if(!j||J)return;if(_=!1,j=!1,cancelAnimationFrame(y),t.size>0){let{nodeLookup:p,updateNodePositions:O,onNodeDragStop:m,onSelectionDragStop:X}=l();if(Q)O(t,!1),Q=!1;if(n||m||!Z&&X){let[v,T]=m5({nodeId:Z,dragItems:t,nodeLookup:p,dragging:!1});if(n?.(S.sourceEvent,t,v,T),m?.(S.sourceEvent,v,T),!Z)X?.(S.sourceEvent,T)}}}).filter((S)=>{let p=S.target;return!S.button&&(!N||!rq(p,`.${N}`,W))&&(!q||rq(p,q,W))});F.call(V)}function U(){F?.on(".drag",null)}return{update:L,destroy:U}}function wV(u,l,f){let r=[],n={x:u.x-f,y:u.y-f,width:f*2,height:f*2};for(let i of l.values())if(yy(n,Ci(i))>0)r.push(i);return r}var LV=250;function KV(u,l,f,r){let n=[],i=1/0,y=wV(u,f,l+LV);for(let t of y){let _=[...t.internals.handleBounds?.source??[],...t.internals.handleBounds?.target??[]];for(let c of _){if(r.nodeId===c.nodeId&&r.type===c.type&&r.id===c.id)continue;let{x:A,y:j}=mn(t,c,c.position,!0),F=Math.sqrt(Math.pow(A-u.x,2)+Math.pow(j-u.y,2));if(F>l)continue;if(F1){let t=r.type==="source"?"target":"source";return n.find((_)=>_.type===t)??n[0]}return n[0]}function Tq(u,l,f,r,n,i=!1){let y=r.get(u);if(!y)return null;let t=n==="strict"?y.internals.handleBounds?.[l]:[...y.internals.handleBounds?.source??[],...y.internals.handleBounds?.target??[]],_=(f?t?.find((c)=>c.id===f):t?.[0])??null;return _&&i?{..._,...mn(y,_,_.position,!0)}:_}function Eq(u,l){if(u)return u;else if(l?.classList.contains("target"))return"target";else if(l?.classList.contains("source"))return"source";return null}function GV(u,l){let f=null;if(l)f=!0;else if(u&&!l)f=!1;return f}var Zq=()=>!0;function zV(u,{connectionMode:l,connectionRadius:f,handleId:r,nodeId:n,edgeUpdaterType:i,isTarget:y,domNode:t,nodeLookup:_,lib:c,autoPanOnConnect:A,flowId:j,panBy:F,cancelConnection:J,onConnectStart:Q,onConnect:w,onConnectEnd:L,isValidConnection:U=Zq,onReconnectEnd:N,updateConnection:q,getTransform:W,getFromHandle:z,autoPanSpeed:Z,dragThreshold:H=1,handleDomNode:E}){let D=l9(u.target),h=0,V,{x:S,y:p}=Or(u),O=Eq(i,E),m=t?.getBoundingClientRect(),X=!1;if(!m||!O)return;let v=Tq(n,O,r,_,l);if(!v)return;let T=Or(u,m),Y=!1,k=null,I=!1,b=null;function o(){if(!A||!m)return;let[Wu,P]=Aq(T,m,Z);F({x:Wu,y:P}),h=requestAnimationFrame(o)}let g={...v,nodeId:n,type:O,position:v.position},x=_.get(n),_u={inProgress:!0,isValid:null,from:mn(x,g,Lu.Left,!0),fromHandle:g,fromPosition:g.position,fromNode:x,to:T,toHandle:null,toPosition:IN[g.position],toNode:null,pointer:T};function $u(){X=!0,q(_u),Q?.(u,{nodeId:n,handleId:r,handleType:O})}if(H===0)$u();function ju(Wu){if(!X){let{x:Nu,y:Eu}=Or(Wu),Hu=Nu-S,vu=Eu-p;if(!(Hu*Hu+vu*vu>H*H))return;$u()}if(!z()||!g){zu(Wu);return}let P=W();if(T=Or(Wu,m),V=KV(_y(T,P,!1,[1,1]),f,_,g),!Y)o(),Y=!0;let e=Oq(Wu,{handle:V,connectionMode:l,fromNodeId:n,fromHandleId:r,fromType:y?"target":"source",isValidConnection:U,doc:D,lib:c,flowId:j,nodeLookup:_});b=e.handleDomNode,k=e.connection,I=GV(!!V,e.isValid);let uu=_.get(n),Ku=uu?mn(uu,g,Lu.Left,!0):_u.from,s={..._u,from:Ku,isValid:I,to:e.toHandle&&I?T_({x:e.toHandle.x,y:e.toHandle.y},P):T,toHandle:e.toHandle,toPosition:I&&e.toHandle?e.toHandle.position:IN[g.position],toNode:e.toHandle?_.get(e.toHandle.nodeId):null,pointer:T};q(s),_u=s}function zu(Wu){if("touches"in Wu&&Wu.touches.length>0)return;if(X){if((V||b)&&k&&I)w?.(k);let{inProgress:P,...e}=_u,uu={...e,toPosition:_u.toHandle?_u.toPosition:null};if(L?.(Wu,uu),i)N?.(Wu,uu)}J(),cancelAnimationFrame(h),Y=!1,I=!1,k=null,b=null,D.removeEventListener("mousemove",ju),D.removeEventListener("mouseup",zu),D.removeEventListener("touchmove",ju),D.removeEventListener("touchend",zu)}D.addEventListener("mousemove",ju),D.addEventListener("mouseup",zu),D.addEventListener("touchmove",ju),D.addEventListener("touchend",zu)}function Oq(u,{handle:l,connectionMode:f,fromNodeId:r,fromHandleId:n,fromType:i,doc:y,lib:t,flowId:_,isValidConnection:c=Zq,nodeLookup:A}){let j=i==="target",F=l?y.querySelector(`.${t}-flow__handle[data-id="${_}-${l?.nodeId}-${l?.id}-${l?.type}"]`):null,{x:J,y:Q}=Or(u),w=y.elementFromPoint(J,Q),L=w?.classList.contains(`${t}-flow__handle`)?w:F,U={handleDomNode:L,isValid:!1,connection:null,toHandle:null};if(L){let N=Eq(void 0,L),q=L.getAttribute("data-nodeid"),W=L.getAttribute("data-handleid"),z=L.classList.contains("connectable"),Z=L.classList.contains("connectableend");if(!q||!N)return U;let H={source:j?q:r,sourceHandle:j?W:n,target:j?r:q,targetHandle:j?n:W};U.connection=H;let D=z&&Z&&(f===Yn.Strict?j&&N==="source"||!j&&N==="target":q!==r||W!==n);U.isValid=D&&c(H),U.toHandle=Tq(q,N,W,A,f,!0)}return U}var f6={onPointerDown:zV,isValid:Oq};function Hq({domNode:u,panZoom:l,getTransform:f,getViewScale:r}){let n=sl(u);function i({translateExtent:t,width:_,height:c,zoomStep:A=1,pannable:j=!0,zoomable:F=!0,inversePan:J=!1}){let Q=(q)=>{if(q.sourceEvent.type!=="wheel"||!l)return;let W=f(),z=q.sourceEvent.ctrlKey&&$y()?10:1,Z=-q.sourceEvent.deltaY*(q.sourceEvent.deltaMode===1?0.05:q.sourceEvent.deltaMode?1:0.002)*A,H=W[2]*Math.pow(2,Z*z);l.scaleTo(H)},w=[0,0],L=(q)=>{if(q.sourceEvent.type==="mousedown"||q.sourceEvent.type==="touchstart")w=[q.sourceEvent.clientX??q.sourceEvent.touches[0].clientX,q.sourceEvent.clientY??q.sourceEvent.touches[0].clientY]},U=(q)=>{let W=f();if(q.sourceEvent.type!=="mousemove"&&q.sourceEvent.type!=="touchmove"||!l)return;let z=[q.sourceEvent.clientX??q.sourceEvent.touches[0].clientX,q.sourceEvent.clientY??q.sourceEvent.touches[0].clientY],Z=[z[0]-w[0],z[1]-w[1]];w=z;let H=r()*Math.max(W[2],Math.log(W[2]))*(J?-1:1),E={x:W[0]-Z[0]*H,y:W[1]-Z[1]*H},D=[[0,0],[_,c]];l.setViewportConstrained({x:E.x,y:E.y,zoom:W[2]},D,t)},N=G_().on("start",L).on("zoom",j?U:null).on("zoom.wheel",F?Q:null);n.call(N,{})}function y(){n.on("zoom",null)}return{update:i,destroy:y,pointer:Gf}}var r6=(u)=>({x:u.x,y:u.y,zoom:u.k}),P5=({x:u,y:l,zoom:f})=>Yi.translate(u,l).scale(f),ly=(u,l)=>u.target.closest(`.${l}`),Bq=(u,l)=>l===2&&Array.isArray(u)&&u.includes(2),TV=(u)=>((u*=2)<=1?u*u*u:(u-=2)*u*u+2)/2,C5=(u,l=0,f=TV,r=()=>{})=>{let n=typeof l==="number"&&l>0;if(!n)r();return n?u.transition().duration(l).ease(f).on("end",r):u},Vq=(u)=>{let l=u.ctrlKey&&$y()?10:1;return-u.deltaY*(u.deltaMode===1?0.05:u.deltaMode?1:0.002)*l};function EV({zoomPanValues:u,noWheelClassName:l,d3Selection:f,d3Zoom:r,panOnScrollMode:n,panOnScrollSpeed:i,zoomOnPinch:y,onPanZoomStart:t,onPanZoom:_,onPanZoomEnd:c}){return(A)=>{if(ly(A,l)){if(A.ctrlKey)A.preventDefault();return!1}A.preventDefault(),A.stopImmediatePropagation();let j=f.property("__zoom").k||1;if(A.ctrlKey&&y){let L=Gf(A),U=Vq(A),N=j*Math.pow(2,U);r.scaleTo(f,N,L,A);return}let F=A.deltaMode===1?20:1,J=n===x0.Vertical?0:A.deltaX*F,Q=n===x0.Horizontal?0:A.deltaY*F;if(!$y()&&A.shiftKey&&n!==x0.Vertical)J=A.deltaY*F,Q=0;r.translateBy(f,-(J/j)*i,-(Q/j)*i,{internal:!0});let w=r6(f.property("__zoom"));if(clearTimeout(u.panScrollTimeout),!u.isPanScrolling)u.isPanScrolling=!0,t?.(A,w);else _?.(A,w),u.panScrollTimeout=setTimeout(()=>{c?.(A,w),u.isPanScrolling=!1},150)}}function ZV({noWheelClassName:u,preventScrolling:l,d3ZoomHandler:f}){return function(r,n){let i=r.type==="wheel",y=!l&&i&&!r.ctrlKey,t=ly(r,u);if(r.ctrlKey&&i&&t)r.preventDefault();if(y||t)return null;r.preventDefault(),f.call(this,r,n)}}function OV({zoomPanValues:u,onDraggingChange:l,onPanZoomStart:f}){return(r)=>{if(r.sourceEvent?.internal)return;let n=r6(r.transform);if(u.mouseButton=r.sourceEvent?.button||0,u.isZoomingOrPanning=!0,u.prevViewport=n,r.sourceEvent?.type==="mousedown")l(!0);if(f)f?.(r.sourceEvent,n)}}function HV({zoomPanValues:u,panOnDrag:l,onPaneContextMenu:f,onTransformChange:r,onPanZoom:n}){return(i)=>{if(u.usedRightMouseButton=!!(f&&Bq(l,u.mouseButton??0)),!i.sourceEvent?.sync)r([i.transform.x,i.transform.y,i.transform.k]);if(n&&!i.sourceEvent?.internal)n?.(i.sourceEvent,r6(i.transform))}}function BV({zoomPanValues:u,panOnDrag:l,panOnScroll:f,onDraggingChange:r,onPanZoomEnd:n,onPaneContextMenu:i}){return(y)=>{if(y.sourceEvent?.internal)return;if(u.isZoomingOrPanning=!1,i&&Bq(l,u.mouseButton??0)&&!u.usedRightMouseButton&&y.sourceEvent)i(y.sourceEvent);if(u.usedRightMouseButton=!1,r(!1),n){let t=r6(y.transform);u.prevViewport=t,clearTimeout(u.timerId),u.timerId=setTimeout(()=>{n?.(y.sourceEvent,t)},f?150:0)}}}function VV({zoomActivationKeyPressed:u,zoomOnScroll:l,zoomOnPinch:f,panOnDrag:r,panOnScroll:n,zoomOnDoubleClick:i,userSelectionActive:y,noWheelClassName:t,noPanClassName:_,lib:c,connectionInProgress:A}){return(j)=>{let F=u||l,J=f&&j.ctrlKey,Q=j.type==="wheel";if(j.button===1&&j.type==="mousedown"&&(ly(j,`${c}-flow__node`)||ly(j,`${c}-flow__edge`)))return!0;if(!r&&!F&&!n&&!i&&!f)return!1;if(y)return!1;if(A&&!Q)return!1;if(ly(j,t)&&Q)return!1;if(ly(j,_)&&(!Q||n&&Q&&!u))return!1;if(!f&&j.ctrlKey&&Q)return!1;if(!f&&j.type==="touchstart"&&j.touches?.length>1)return j.preventDefault(),!1;if(!F&&!n&&!J&&Q)return!1;if(!r&&(j.type==="mousedown"||j.type==="touchstart"))return!1;if(Array.isArray(r)&&!r.includes(j.button)&&j.type==="mousedown")return!1;let w=Array.isArray(r)&&r.includes(j.button)||!j.button||j.button<=1;return(!j.ctrlKey||Q)&&w}}function Dq({domNode:u,minZoom:l,maxZoom:f,translateExtent:r,viewport:n,onPanZoom:i,onPanZoomStart:y,onPanZoomEnd:t,onDraggingChange:_}){let c={isZoomingOrPanning:!1,usedRightMouseButton:!1,prevViewport:{x:0,y:0,zoom:0},mouseButton:0,timerId:void 0,panScrollTimeout:void 0,isPanScrolling:!1},A=u.getBoundingClientRect(),j=G_().scaleExtent([l,f]).translateExtent(r),F=sl(u).call(j);N({x:n.x,y:n.y,zoom:fy(n.zoom,l,f)},[[0,0],[A.width,A.height]],r);let J=F.on("wheel.zoom"),Q=F.on("dblclick.zoom");j.wheelDelta(Vq);function w(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).transform(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function L({noWheelClassName:V,noPanClassName:S,onPaneContextMenu:p,userSelectionActive:O,panOnScroll:m,panOnDrag:X,panOnScrollMode:v,panOnScrollSpeed:T,preventScrolling:Y,zoomOnPinch:k,zoomOnScroll:I,zoomOnDoubleClick:b,zoomActivationKeyPressed:o,lib:g,onTransformChange:x,connectionInProgress:lu,paneClickDistance:_u,selectionOnDrag:$u}){if(O&&!c.isZoomingOrPanning)U();let ju=m&&!o&&!O;j.clickDistance($u?1/0:!Zr(_u)||_u<0?0:_u);let zu=ju?EV({zoomPanValues:c,noWheelClassName:V,d3Selection:F,d3Zoom:j,panOnScrollMode:v,panOnScrollSpeed:T,zoomOnPinch:k,onPanZoomStart:y,onPanZoom:i,onPanZoomEnd:t}):ZV({noWheelClassName:V,preventScrolling:Y,d3ZoomHandler:J});if(F.on("wheel.zoom",zu,{passive:!1}),!O){let P=OV({zoomPanValues:c,onDraggingChange:_,onPanZoomStart:y});j.on("start",P);let e=HV({zoomPanValues:c,panOnDrag:X,onPaneContextMenu:!!p,onPanZoom:i,onTransformChange:x});j.on("zoom",e);let uu=BV({zoomPanValues:c,panOnDrag:X,panOnScroll:m,onPaneContextMenu:p,onPanZoomEnd:t,onDraggingChange:_});j.on("end",uu)}let Wu=VV({zoomActivationKeyPressed:o,panOnDrag:X,zoomOnScroll:I,panOnScroll:m,zoomOnDoubleClick:b,zoomOnPinch:k,userSelectionActive:O,noPanClassName:S,noWheelClassName:V,lib:g,connectionInProgress:lu});if(j.filter(Wu),b)F.on("dblclick.zoom",Q);else F.on("dblclick.zoom",null)}function U(){j.on("zoom",null)}async function N(V,S,p){let O=P5(V),m=j?.constrain()(O,S,p);if(m)await w(m);return new Promise((X)=>X(m))}async function q(V,S){let p=P5(V);return await w(p,S),new Promise((O)=>O(p))}function W(V){if(F){let S=P5(V),p=F.property("__zoom");if(p.k!==V.zoom||p.x!==V.x||p.y!==V.y)j?.transform(F,S,null,{sync:!0})}}function z(){let V=F?K_(F.node()):{x:0,y:0,k:1};return{x:V.x,y:V.y,zoom:V.k}}function Z(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).scaleTo(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function H(V,S){if(F)return new Promise((p)=>{j?.interpolate(S?.interpolate==="linear"?$0:Xi).scaleBy(C5(F,S?.duration,S?.ease,()=>p(!0)),V)});return Promise.resolve(!1)}function E(V){j?.scaleExtent(V)}function D(V){j?.translateExtent(V)}function h(V){let S=!Zr(V)||V<0?0:V;j?.clickDistance(S)}return{update:L,destroy:U,setViewport:q,setViewportConstrained:N,getViewport:z,scaleTo:Z,scaleBy:H,setScaleExtent:E,setTranslateExtent:D,syncViewport:W,setClickDistance:h}}var Pn;(function(u){u.Line="line",u.Handle="handle"})(Pn||(Pn={}));function DV({width:u,prevWidth:l,height:f,prevHeight:r,affectsX:n,affectsY:i}){let y=u-l,t=f-r,_=[y>0?1:y<0?-1:0,t>0?1:t<0?-1:0];if(y&&n)_[0]=_[0]*-1;if(t&&i)_[1]=_[1]*-1;return _}function nq(u){let l=u.includes("right")||u.includes("left"),f=u.includes("bottom")||u.includes("top"),r=u.includes("left"),n=u.includes("top");return{isHorizontal:l,isVertical:f,affectsX:r,affectsY:n}}function Xn(u,l){return Math.max(0,l-u)}function Sn(u,l){return Math.max(0,u-l)}function bc(u,l,f){return Math.max(0,l-u,u-f)}function iq(u,l){return u?!l:l}function XV(u,l,f,r,n,i,y,t){let{affectsX:_,affectsY:c}=l,{isHorizontal:A,isVertical:j}=l,F=A&&j,{xSnapped:J,ySnapped:Q}=f,{minWidth:w,maxWidth:L,minHeight:U,maxHeight:N}=r,{x:q,y:W,width:z,height:Z,aspectRatio:H}=u,E=Math.floor(A?J-u.pointerX:0),D=Math.floor(j?Q-u.pointerY:0),h=z+(_?-E:E),V=Z+(c?-D:D),S=-i[0]*z,p=-i[1]*Z,O=bc(h,w,L),m=bc(V,U,N);if(y){let T=0,Y=0;if(_&&E<0)T=Xn(q+E+S,y[0][0]);else if(!_&&E>0)T=Sn(q+h+S,y[1][0]);if(c&&D<0)Y=Xn(W+D+p,y[0][1]);else if(!c&&D>0)Y=Sn(W+V+p,y[1][1]);O=Math.max(O,T),m=Math.max(m,Y)}if(t){let T=0,Y=0;if(_&&E>0)T=Sn(q+E,t[0][0]);else if(!_&&E<0)T=Xn(q+h,t[1][0]);if(c&&D>0)Y=Sn(W+D,t[0][1]);else if(!c&&D<0)Y=Xn(W+V,t[1][1]);O=Math.max(O,T),m=Math.max(m,Y)}if(n){if(A){let T=bc(h/H,U,N)*H;if(O=Math.max(O,T),y){let Y=0;if(!_&&!c||_&&!c&&F)Y=Sn(W+p+h/H,y[1][1])*H;else Y=Xn(W+p+(_?E:-E)/H,y[0][1])*H;O=Math.max(O,Y)}if(t){let Y=0;if(!_&&!c||_&&!c&&F)Y=Xn(W+h/H,t[1][1])*H;else Y=Sn(W+(_?E:-E)/H,t[0][1])*H;O=Math.max(O,Y)}}if(j){let T=bc(V*H,w,L)/H;if(m=Math.max(m,T),y){let Y=0;if(!_&&!c||c&&!_&&F)Y=Sn(q+V*H+S,y[1][0])/H;else Y=Xn(q+(c?D:-D)*H+S,y[0][0])/H;m=Math.max(m,Y)}if(t){let Y=0;if(!_&&!c||c&&!_&&F)Y=Xn(q+V*H,t[1][0])/H;else Y=Sn(q+(c?D:-D)*H,t[0][0])/H;m=Math.max(m,Y)}}}if(D=D+(D<0?m:-m),E=E+(E<0?O:-O),n)if(F)if(h>V*H)D=(iq(_,c)?-E:E)/H;else E=(iq(_,c)?-D:D)*H;else if(A)D=E/H,c=_;else E=D*H,_=c;let X=_?q+E:q,v=c?W+D:W;return{width:z+(_?-E:E),height:Z+(c?-D:D),x:i[0]*E*(!_?1:-1)+X,y:i[1]*D*(!c?1:-1)+v}}var Xq={width:0,height:0,x:0,y:0},SV={...Xq,pointerX:0,pointerY:0,aspectRatio:1};function YV(u){return[[0,0],[u.measured.width,u.measured.height]]}function pV(u,l,f){let r=l.position.x+u.position.x,n=l.position.y+u.position.y,i=u.measured.width??0,y=u.measured.height??0,t=f[0]*i,_=f[1]*y;return[[r-t,n-_],[r+i-t,n+y-_]]}function Sq({domNode:u,nodeId:l,getStoreItems:f,onChange:r,onEnd:n}){let i=sl(u),y={controlDirection:nq("bottom-right"),boundaries:{minWidth:0,minHeight:0,maxWidth:Number.MAX_VALUE,maxHeight:Number.MAX_VALUE},resizeDirection:void 0,keepAspectRatio:!1};function t({controlPosition:c,boundaries:A,keepAspectRatio:j,resizeDirection:F,onResizeStart:J,onResize:Q,onResizeEnd:w,shouldResize:L}){let U={...Xq},N={...SV};y={boundaries:A,resizeDirection:F,keepAspectRatio:j,controlDirection:nq(c)};let q=void 0,W=null,z=[],Z=void 0,H=void 0,E=void 0,D=!1,h=i_().on("start",(V)=>{let{nodeLookup:S,transform:p,snapGrid:O,snapToGrid:m,nodeOrigin:X,paneDomNode:v}=f();if(q=S.get(l),!q)return;W=v?.getBoundingClientRect()??null;let{xSnapped:T,ySnapped:Y}=z_(V.sourceEvent,{transform:p,snapGrid:O,snapToGrid:m,containerBounds:W});if(U={width:q.measured.width??0,height:q.measured.height??0,x:q.position.x??0,y:q.position.y??0},N={...U,pointerX:T,pointerY:Y,aspectRatio:U.width/U.height},Z=void 0,q.parentId&&(q.extent==="parent"||q.expandParent))Z=S.get(q.parentId),H=Z&&q.extent==="parent"?YV(Z):void 0;z=[],E=void 0;for(let[k,I]of S)if(I.parentId===l){if(z.push({id:k,position:{...I.position},extent:I.extent}),I.extent==="parent"||I.expandParent){let b=pV(I,q,I.origin??X);if(E)E=[[Math.min(b[0][0],E[0][0]),Math.min(b[0][1],E[0][1])],[Math.max(b[1][0],E[1][0]),Math.max(b[1][1],E[1][1])]];else E=b}}J?.(V,{...U})}).on("drag",(V)=>{let{transform:S,snapGrid:p,snapToGrid:O,nodeOrigin:m}=f(),X=z_(V.sourceEvent,{transform:S,snapGrid:p,snapToGrid:O,containerBounds:W}),v=[];if(!q)return;let{x:T,y:Y,width:k,height:I}=U,b={},o=q.origin??m,{width:g,height:x,x:lu,y:_u}=XV(N,y.controlDirection,X,y.boundaries,y.keepAspectRatio,o,H,E),$u=g!==k,ju=x!==I,zu=lu!==T&&$u,Wu=_u!==Y&&ju;if(!zu&&!Wu&&!$u&&!ju)return;if(zu||Wu||o[0]===1||o[1]===1){if(b.x=zu?lu:U.x,b.y=Wu?_u:U.y,U.x=b.x,U.y=b.y,z.length>0){let Ku=lu-T,s=_u-Y;for(let Nu of z)Nu.position={x:Nu.position.x-Ku+o[0]*(g-k),y:Nu.position.y-s+o[1]*(x-I)},v.push(Nu)}}if($u||ju)b.width=$u&&(!y.resizeDirection||y.resizeDirection==="horizontal")?g:U.width,b.height=ju&&(!y.resizeDirection||y.resizeDirection==="vertical")?x:U.height,U.width=b.width,U.height=b.height;if(Z&&q.expandParent){let Ku=o[0]*(b.width??0);if(b.x&&b.x{if(!D)return;w?.(V,{...U}),n?.({...U}),D=!1});i.call(h)}function _(){i.on(".drag",null)}return{update:t,destroy:_}}var kq=Pu(Jl(),1),Iq=Pu(xq(),1);var hq=(u)=>{let l,f=new Set,r=(A,j)=>{let F=typeof A==="function"?A(l):A;if(!Object.is(F,l)){let J=l;l=(j!=null?j:typeof F!=="object"||F===null)?F:Object.assign({},l,F),f.forEach((Q)=>Q(l,J))}},n=()=>l,_={setState:r,getState:n,getInitialState:()=>c,subscribe:(A)=>{return f.add(A),()=>f.delete(A)},destroy:()=>{f.clear()}},c=l=u(r,n,_);return _},bq=(u)=>u?hq(u):hq;var{useDebugValue:uD}=kq.default,{useSyncExternalStoreWithSelector:lD}=Iq.default,fD=(u)=>u;function j9(u,l=fD,f){let r=lD(u.subscribe,u.getState,u.getServerState||u.getInitialState,l,f);return uD(r),r}var vq=(u,l)=>{let f=bq(u),r=(n,i=l)=>j9(f,n,i);return Object.assign(r,f),r},gq=(u,l)=>u?vq(u,l):vq;function Gl(u,l){if(Object.is(u,l))return!0;if(typeof u!=="object"||u===null||typeof l!=="object"||l===null)return!1;if(u instanceof Map&&l instanceof Map){if(u.size!==l.size)return!1;for(let[r,n]of u)if(!Object.is(n,l.get(r)))return!1;return!0}if(u instanceof Set&&l instanceof Set){if(u.size!==l.size)return!1;for(let r of u)if(!l.has(r))return!1;return!0}let f=Object.keys(u);if(f.length!==Object.keys(l).length)return!1;for(let r of f)if(!Object.prototype.hasOwnProperty.call(l,r)||!Object.is(u[r],l[r]))return!1;return!0}var rD=Pu(WA(),1),_6=yu.createContext(null),nD=_6.Provider,qW=Ur.error001();function fl(u,l){let f=yu.useContext(_6);if(f===null)throw Error(qW);return j9(f,u,l)}function El(){let u=yu.useContext(_6);if(u===null)throw Error(qW);return yu.useMemo(()=>({getState:u.getState,setState:u.setState,subscribe:u.subscribe}),[u])}var sq={display:"none"},iD={position:"absolute",width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0px, 0px, 0px, 0px)",clipPath:"inset(100%)"},WW="react-flow__node-desc",wW="react-flow__edge-desc",yD="react-flow__aria-live",tD=(u)=>u.ariaLiveMessage,_D=(u)=>u.ariaLabelConfig;function $D({rfId:u}){let l=fl(tD);return ru.jsx("div",{id:`${yD}-${u}`,"aria-live":"assertive","aria-atomic":"true",style:iD,children:l})}function cD({rfId:u,disableKeyboardA11y:l}){let f=fl(_D);return ru.jsxs(ru.Fragment,{children:[ru.jsx("div",{id:`${WW}-${u}`,style:sq,children:l?f["node.a11yDescription.default"]:f["node.a11yDescription.keyboardDisabled"]}),ru.jsx("div",{id:`${wW}-${u}`,style:sq,children:f["edge.a11yDescription.default"]}),!l&&ru.jsx($D,{rfId:u})]})}var $6=yu.forwardRef(({position:u="top-left",children:l,className:f,style:r,...n},i)=>{let y=`${u}`.split("-");return ru.jsx("div",{className:Sl(["react-flow__panel",f,...y]),style:r,ref:i,...n,children:l})});$6.displayName="Panel";function AD({proOptions:u,position:l="bottom-right"}){if(u?.hideAttribution)return null;return ru.jsx($6,{position:l,className:"react-flow__attribution","data-message":"Please only hide this attribution when you are subscribed to React Flow Pro: https://pro.reactflow.dev",children:ru.jsx("a",{href:"https://reactflow.dev",target:"_blank",rel:"noopener noreferrer","aria-label":"React Flow attribution",children:"React Flow"})})}var jD=(u)=>{let l=[],f=[];for(let[,r]of u.nodeLookup)if(r.selected)l.push(r.internals.userNode);for(let[,r]of u.edgeLookup)if(r.selected)f.push(r);return{selectedNodes:l,selectedEdges:f}},i6=(u)=>u.id;function FD(u,l){return Gl(u.selectedNodes.map(i6),l.selectedNodes.map(i6))&&Gl(u.selectedEdges.map(i6),l.selectedEdges.map(i6))}function UD({onSelectionChange:u}){let l=El(),{selectedNodes:f,selectedEdges:r}=fl(jD,FD);return yu.useEffect(()=>{let n={nodes:f,edges:r};u?.(n),l.getState().onSelectionChangeHandlers.forEach((i)=>i(n))},[f,r,u]),null}var JD=(u)=>!!u.onSelectionChangeHandlers;function QD({onSelectionChange:u}){let l=fl(JD);if(u||l)return ru.jsx(UD,{onSelectionChange:u});return null}var J9=typeof window<"u"?yu.useLayoutEffect:yu.useEffect,LW=[0,0],ND={x:0,y:0,zoom:1},qD=["nodes","edges","defaultNodes","defaultEdges","onConnect","onConnectStart","onConnectEnd","onClickConnectStart","onClickConnectEnd","nodesDraggable","autoPanOnNodeFocus","nodesConnectable","nodesFocusable","edgesFocusable","edgesReconnectable","elevateNodesOnSelect","elevateEdgesOnSelect","minZoom","maxZoom","nodeExtent","onNodesChange","onEdgesChange","elementsSelectable","connectionMode","snapGrid","snapToGrid","translateExtent","connectOnClick","defaultEdgeOptions","fitView","fitViewOptions","onNodesDelete","onEdgesDelete","onDelete","onNodeDrag","onNodeDragStart","onNodeDragStop","onSelectionDrag","onSelectionDragStart","onSelectionDragStop","onMoveStart","onMove","onMoveEnd","noPanClassName","nodeOrigin","autoPanOnConnect","autoPanOnNodeDrag","onError","connectionRadius","isValidConnection","selectNodesOnDrag","nodeDragThreshold","connectionDragThreshold","onBeforeDelete","debug","autoPanSpeed","ariaLabelConfig","zIndexMode"],aq=[...qD,"rfId"],WD=(u)=>({setNodes:u.setNodes,setEdges:u.setEdges,setMinZoom:u.setMinZoom,setMaxZoom:u.setMaxZoom,setTranslateExtent:u.setTranslateExtent,setNodeExtent:u.setNodeExtent,reset:u.reset,setDefaultNodesAndEdges:u.setDefaultNodesAndEdges}),oq={translateExtent:ny,nodeOrigin:LW,minZoom:0.5,maxZoom:2,elementsSelectable:!0,noPanClassName:"nopan",rfId:"1"};function wD(u){let{setNodes:l,setEdges:f,setMinZoom:r,setMaxZoom:n,setTranslateExtent:i,setNodeExtent:y,reset:t,setDefaultNodesAndEdges:_}=fl(WD,Gl),c=El();J9(()=>{return _(u.defaultNodes,u.defaultEdges),()=>{A.current=oq,t()}},[]);let A=yu.useRef(oq);return J9(()=>{for(let j of aq){let F=u[j],J=A.current[j];if(F===J)continue;if(typeof u[j]>"u")continue;if(j==="nodes")l(F);else if(j==="edges")f(F);else if(j==="minZoom")r(F);else if(j==="maxZoom")n(F);else if(j==="translateExtent")i(F);else if(j==="nodeExtent")y(F);else if(j==="ariaLabelConfig")c.setState({ariaLabelConfig:Fq(F)});else if(j==="fitView")c.setState({fitViewQueued:F});else if(j==="fitViewOptions")c.setState({fitViewOptions:F});else c.setState({[j]:F})}A.current=u},aq.map((j)=>u[j])),null}function dq(){if(typeof window>"u"||!window.matchMedia)return null;return window.matchMedia("(prefers-color-scheme: dark)")}function LD(u){let[l,f]=yu.useState(u==="system"?null:u);return yu.useEffect(()=>{if(u!=="system"){f(u);return}let r=dq(),n=()=>f(r?.matches?"dark":"light");return n(),r?.addEventListener("change",n),()=>{r?.removeEventListener("change",n)}},[u]),l!==null?l:dq()?.matches?"dark":"light"}var eq=typeof document<"u"?document:null;function H_(u=null,l={target:eq,actInsideInputWithModifier:!0}){let[f,r]=yu.useState(!1),n=yu.useRef(!1),i=yu.useRef(new Set([])),[y,t]=yu.useMemo(()=>{if(u!==null){let c=(Array.isArray(u)?u:[u]).filter((j)=>typeof j==="string").map((j)=>j.replace("+",` `).replace(` `,` @@ -286,4 +286,4 @@ html,body{width:${r}px!important;min-height:${n}px!important;overflow:hidden!imp ${m} ${O} ${X} - `,width:w,height:L}}function K6(u,l){let f=URL.createObjectURL(u),r=document.createElement("a");r.href=f,r.download=l,r.click(),setTimeout(()=>URL.revokeObjectURL(f),1000)}async function Xw(u,l){let f=Dw(l,"pipeline"),{svg:r,width:n,height:i}=uY(u,l),y=new Blob([r],{type:"image/svg+xml;charset=utf-8"}),t=URL.createObjectURL(y);try{let _=new Image;await new Promise((F,J)=>{_.onload=()=>F(),_.onerror=()=>J(Error("svg image load failed")),_.src=t});let c=document.createElement("canvas");c.width=n,c.height=i;let A=c.getContext("2d");if(!A)throw Error("canvas unavailable");A.drawImage(_,0,0);let j=await new Promise((F)=>c.toBlob(F,"image/png"));if(!j)throw Error("png export failed");K6(j,`${f}.png`)}catch{K6(y,`${f}.svg`)}finally{URL.revokeObjectURL(t)}}async function iY(u){let l=Dw(String(u?.title||"pipeline-gantt"),"pipeline-gantt"),{svg:f,width:r,height:n}=nY(u),i=new Blob([f],{type:"image/svg+xml;charset=utf-8"}),y=URL.createObjectURL(i);try{let t=new Image;await new Promise((j,F)=>{t.onload=()=>j(),t.onerror=()=>F(Error("gantt svg image load failed")),t.src=y});let _=document.createElement("canvas");_.width=r,_.height=n;let c=_.getContext("2d");if(!c)throw Error("canvas unavailable");c.drawImage(t,0,0);let A=await new Promise((j)=>_.toBlob(j,"image/png"));if(!A)throw Error("gantt png export failed");K6(A,`${l}.png`)}catch{K6(i,`${l}.svg`)}finally{URL.revokeObjectURL(y)}}async function yY(u){for(let l of u){if(l.flow.nodes.length===0)continue;await Xw(l.flow,l.title),await new Promise((f)=>setTimeout(f,750))}}function Qw(u,l){return u.find((f)=>String(f?.pipelineId||"")===l)||null}function Nw(u){return Iu(u?.startedAt)??Iu(u?.artifact?.startedAt)??Iu(u?.request?.createdAt)??Iu(u?.updatedAt)??0}function tY(u,l){return u.filter((f)=>String(f?.pipelineId||"")===l).slice().sort((f,r)=>Nw(f)-Nw(r)||String(f?.runId||"").localeCompare(String(r?.runId||"")))}function X9(u,l){let f=String(l?.runId||""),r=u.findIndex((y)=>String(y?.runId||"")===f),n=r>=0?r+1:u.length,i=String(l?.status||"--");return`Epoch ${n} / ${f||"--"} / ${i}`}function Vr(u){return String(u?.procedureRunId||u?.runId||"")}function E6(u,l){let f=String(u?.nodeId||u?.request?.nodeId||"");if(f)return f;let r=Vr(u),n=`${l}__`;if(r.startsWith(n))return r.slice(n.length).replace(/__\d+$/u,"");return""}function F6(u,l){let f=Xu(u?.artifact)?u.artifact:{},r=Xu(u?.request)?u.request:{};return C_(u?.startedAt,f.startedAt,r.createdAt,r.startedAt,u?.createdAt,u?.updatedAt,l?.startedAt,l?.request?.createdAt)}function U6(u,l){let f=String(u?.status?.status||u?.artifact?.status||u?.status||"").toLowerCase(),r=Xu(u?.artifact)?u.artifact:{},n=m9(f);return C_(u?.finishedAt,r.finishedAt,u?.completedAt,n?u?.updatedAt:void 0,n?r.updatedAt:void 0,n?l?.updatedAt:void 0)}function Sw(u,l,f=Date.now()){let r=String(u?.runId||""),n=new Set(l.map((i)=>String(i?.id||"")).filter(Boolean));return Du(u?.procedureRuns).flatMap((i)=>{let y=E6(i,r);if(!y)return[];let t=String(i?.status?.status||i?.artifact?.status||i?.status||"unknown").toLowerCase(),_=F6(i,u),c=Iu(_);if(c===null)return[];let A=U6(i,u),j=Iu(A)??(m9(t)?Iu(i?.updatedAt)??c+1000:f),F=Math.max(c+1000,j);return[{nodeId:y,knownNode:n.has(y),procedureRunId:Vr(i),status:t,startMs:c,endMs:F,startedAt:p_(c),finishedAt:p_(F),durationMs:F-c,runId:r,raw:i}]}).sort((i,y)=>i.startMs-y.startMs||i.endMs-y.endMs||i.nodeId.localeCompare(y.nodeId))}function _Y(u,l,f=[]){let r=l.map((A)=>Number(A.startMs)).filter(Number.isFinite),n=l.map((A)=>Number(A.endMs)).filter(Number.isFinite);for(let A of f){let j=Vl(A?.eventMs??A?.ms);if(j!==null)r.push(j),n.push(j)}let i=Iu(u?.startedAt)??Iu(u?.artifact?.startedAt)??Iu(u?.request?.createdAt),y=Iu(u?.finishedAt)??Iu(u?.artifact?.finishedAt)??Iu(u?.updatedAt);if(i!==null)r.push(i);if(y!==null)n.push(y);let t=Date.now(),_=r.length>0?Math.min(...r):t-60000,c=Math.max(_+60000,n.length>0?Math.max(...n):t);return{startMs:_,endMs:c,durationMs:c-_}}var J6=12,Yw=20,S9=100,$Y=!1;function vn(u){let l=Number(u);if(!Number.isFinite(l))return 0;return Math.max(0,Math.min(100,Math.round(l*100)/100))}function cY(u){let l=Math.max(J6,Number(u||J6)),f=Math.log(l/J6)/Math.log(Yw);return vn(f*100)}var P_=cY(S9);function h9(u){let l=vn(u)/100,f=J6*Math.pow(Yw,l),r=l<0.24?"全局":l<0.64?"均衡":"细节";return{value:vn(l*100),pxPerMinute:f,label:r}}function E9(u){let l=Math.round(Number(u));return Math.abs(l-S9)<=1?S9:l}function AY(u,l=P_){let f=Math.max(1,Number(u.durationMs||0)/60000),r=h9(l);return Math.round(Math.max(360,Math.min(7200,f*Number(r.pxPerMinute||48))))}function jY(u,l=7){let f=Math.max(1,Number(u.endMs||0)-Number(u.startMs||0));return Array.from({length:l},(r,n)=>{let i=l===1?0:n/(l-1);return{ms:Number(u.startMs)+f*i,percent:i*100}})}function FY(u,l){let f=Math.max(1,Number(l.endMs)-Number(l.startMs));return Math.max(0,Math.min(100,(u-Number(l.startMs))/f*100))}function Vl(u){let l=Number(u);return Number.isFinite(l)?l:null}function b9(u){return Lw(u?.status)&&!m9(u?.status)}function pw(u,l,f,r){let n=Math.max(1,f-l),i=Math.max(0,Math.min(1,(u-l)/n));return Number((i*r).toFixed(3))}function qw(u,l){if(!l)return null;let f=Vl(l?.startMs),r=Vl(l?.endMs),n=Vl(l?.chartHeight);if(f===null||r===null||n===null)return null;return pw(u,f,r,n)}function mw(u,l){let f=Vl(u?.rawStartMs??u?.startMs)??Vl(u?.startMs)??l,r=Vl(u?.endMs)??f+1000;if(!b9(u))return Math.max(f+1000,r);return Math.max(f+1000,r,l)}function UY(u,l,f,r){let n=Vl(u?.startMs)??r-60000,i=Vl(u?.endMs)??r,y=f.reduce((Q,w)=>Math.max(Q,mw(w,r)),i),t=Math.max(n+60000,i,y),_=Math.max(1,t-n),c={startMs:n,endMs:t,durationMs:_},A=AY(c,l),j=h9(l),F=Math.max(5,Math.min(18,Math.round(A/150))),J=jY(c,F).map((Q)=>{let w=Number(Q.ms),L=pw(w,n,t,A);return{...Q,y:L,timestamp:p_(w),offsetMs:w-n}});return{source:"frontend-y",startMs:n,endMs:t,durationMs:_,chartHeight:A,scale:vn(l),normalizedScale:Number((vn(l)/100).toFixed(3)),pxPerMinute:Number(Number(j.pxPerMinute||0).toFixed(3)),ticks:J}}function JY(u,l,f){if(!b9(u))return u;let r=Vl(u?.rawStartMs??u?.startMs)??Vl(u?.startMs)??f,n=mw(u,f),i=qw(r,l),y=qw(n,l),t=Vl(i??u?.y1??u?.startY)??0,_=Vl(y??u?.y2??u?.endY)??t+10,c=Math.max(24,_-t);return{...u,live:!0,startMs:r,endMs:n,durationMs:Math.max(1000,n-r),finishedAt:p_(n),y1:t,y2:_,startY:t,endY:_,height:c}}function v9(u,l,f){return FY(u,l)/100*f}function Qy(u){return Boolean(u&&String(u?.source||"")!=="frontend-y")}function Pw(u,l,f,r,n){if(Qy(r))for(let y of n){let t=Vl(u?.[y]);if(t!==null)return t}let i=Vl(u?.ms??u?.eventMs??u?.startMs);return v9(i??Number(l.startMs),l,f)}function G6(u,l,f,r){return Pw(u,l,f,r,["y1","startY"])}function Y9(u,l,f,r){if(Qy(r)){let i=Vl(u?.y2??u?.endY);if(i!==null)return i}let n=Vl(u?.endMs)??Number(l.endMs);return v9(n,l,f)}function Cw(u,l,f,r){if(Qy(r)){let i=Vl(u?.height);if(i!==null)return Math.max(1,i)}let n=u?.live?24:10;return Math.max(n,Y9(u,l,f,r)-G6(u,l,f,r))}function Jr(u,l,f,r){return Pw(u,l,f,r,["y","timeAxisY"])}function Mw(u,l,f,r){if(Qy(r)||String(r?.source||"")==="frontend-y"){let y=Vl(u?.y);if(y!==null)return y}let n=Vl(u?.percent);if(n!==null)return n/100*f;let i=Vl(u?.ms)??Number(l.startMs);return v9(i,l,f)}function QY(u){let l=String(u?.promptEvent||u?.raw?.promptEvent||u?.event||"").toLowerCase();if(!["node-long-running-observation","node-finished"].includes(l))return"";let f=String(u?.sourceNodeId||u?.raw?.sourceNodeId||u?.raw?.detail?.nodeId||""),r=String(u?.nodeId||u?.targetNodeId||"");return f&&f!==r?f:""}function NY(u,l){let f=new Set(l.map((n)=>[String(n.sourceNodeId||""),String(n.targetNodeId||""),String(n.targetMarkerId||""),String(n.action||"")].join(":"))),r=[...l];for(let n of u){let i=QY(n),y=String(n?.nodeId||""),t=String(n?.id||"");if(!i||!y||!t)continue;let _=[i,y,t,"observe"].join(":");if(f.has(_))continue;f.add(_),r.push({id:`observation-arrow:${t}:${i}:${y}`,commandId:String(n?.commandId||n?.eventId||t),sourceNodeId:i,targetNodeId:y,sourceMarkerId:"",targetMarkerId:t,sourceKind:"monitor",action:"observe",status:"observation"})}return{markers:u,arrows:r}}function qY(u){let l=gr(u),f=String(u?.promptEvent||"");if(l==="initial-prompt-delivered")return"initial";if(f==="node-finished"||f==="node-long-running-observation"||f.startsWith("monitor-"))return"monitor";if(l==="monitor-prompt-delivered"||String(u?.sourceKind||"").toLowerCase()==="monitor")return"monitor";return"append"}function WY(u){return Du(u?.tags||u?.raw?.tags).map((l)=>String(l||"")).filter(Boolean)}function wY(u){let l=gr(u),f=String(u?.promptEvent||"");if(l==="initial-prompt-delivered")return"初始 prompt";if(f==="node-long-running-observation")return"长任务观察";if(f==="node-finished")return WY(u).includes("monitor.audit")?"节点完成 / OA 审核":"节点完成";if(f==="monitor-interval")return"Monitor observation";if(f==="monitor-start")return"Monitor start";if(f==="monitor-stop")return"Monitor stop";if(l==="monitor-prompt-delivered")return"Monitor prompt";if(l==="append-prompt-queued")return"追加 prompt 已排队";return"追加 prompt"}function Ww(u){let l=gr(u);if(l==="control-command-applied")return 3;if(l==="control-command-ignored")return 2;if(l==="control-command-queued")return 1;return 0}function LY(u,l){let f=String(u?.commandId||"");if(f)return`command:${f}`;return["control-event",Fy(u)||C_(u?.createdAt,u?.timestamp)||`index-${l}`,String(u?.sourceKind||""),String(u?.sourceNodeId||""),String(u?.targetNodeId||""),xi(u)].join(":")}function KY(u){return V9([u?.targetNodeId,...Du(u?.resetNodeIds)])}function GY(u,l){let f=S_(u),r=gr(u),n=String(u?.targetNodeId||""),i=Boolean(n)&&l!==n;if(r==="control-command-applied")return i?`${f} 波及`:`${f} 生效`;if(r==="control-command-ignored")return`${f} 忽略`;if(r==="control-command-queued")return`${f} 已发起`;return i?`${f} 波及`:f}function zY(u){if(gr(u)==="control-command-ignored")return"ignored";let f=xi(u);if(f==="restart"||f==="redo")return"restart";if(f==="modify")return"modify";if(f==="approve")return"approve";if(f==="guide")return"guide";return"pending"}function TY(u){let l=String(u?.sourceKind||"").toLowerCase();if(l==="monitor")return"monitor";if(l==="webui")return"webui";if(l==="cli")return"cli";return"system"}function EY(u,l,f,r){let n=u.filter((c)=>String(c.nodeId||"")===l).sort((c,A)=>Number(c.startMs)-Number(A.startMs)),i=n.find((c)=>f>=Number(c.startMs)-1000&&f<=Number(c.endMs)+1000);if(i)return{ms:f,onInterval:!0,snapReason:"inside-interval",procedureRunId:String(i.procedureRunId||"")};let y=xi(r),t=n.slice().reverse().find((c)=>Number(c.endMs)<=f+1000);if(t&&y==="approve")return{ms:Number(t.endMs),onInterval:!0,snapReason:"previous-interval-end",procedureRunId:String(t.procedureRunId||"")};let _=n.find((c)=>Number(c.startMs)>=f-1000);if(_&&["guide","modify","restart","redo"].includes(y))return{ms:Number(_.startMs),onInterval:!0,snapReason:"next-interval-start",procedureRunId:String(_.procedureRunId||"")};return{ms:f,onInterval:!1,snapReason:"event-time",procedureRunId:String(r?.procedureRunId||"")}}function Rw(u,l,f,r){let n=Math.hypot(f-u,r-l),i=n>lw?lw:0,y=i>0?f-(f-u)/n*i:f,t=i>0?r-(r-l)/n*i:r,_=y-u,c=Math.max(16,Math.min(42,Math.abs(_)*0.45+12)),A=_===0?1:Math.sign(_);return`M ${u},${l} C ${u+A*c},${l} ${y-A*c},${t} ${y},${t}`}function ZY(u,l){let f=String(u?.runId||l?.runId||""),r=Sw({...Xu(l)?l:{},...Xu(u)?u:{},runId:f,procedureRuns:Du(u?.procedureRuns).length>0?u.procedureRuns:l?.procedureRuns},[]),n=[],i=[],y=[],t=new Set,_=new Map,c=(F,J)=>{if(!F.nodeId||!Number.isFinite(Number(F.ms)))return;if(t.has(F.id))return;t.add(F.id),J.push(F)};for(let F of Du(u?.procedureRuns)){let J=E6(F,f),Q=Vr(F);if(!J)continue;for(let w of Du(F?.attempts)){let L=T6(w);for(let U of B9(w?.controlEventRecords)){let N=gr(U);if(!["initial-prompt-delivered","append-prompt-delivered","monitor-prompt-delivered"].includes(N))continue;let q=Fy(U),W=Iu(q);if(W===null)continue;let z=String(U?.eventId||"");c({id:`prompt:${z||`${Q}:${L}:${N}:${W}`}`,runId:f,nodeId:J,procedureRunId:Q,attempt:L,kind:"prompt",tone:qY(U),status:"delivered",label:wY(U),ms:W,timestampIso:q,sourceKind:String(U?.sourceKind||""),sourceNodeId:String(U?.sourceNodeId||""),targetNodeId:J,action:"",eventId:z,commandId:String(U?.commandId||""),raw:U},n)}}}let A=new Map;B9(u?.controlEvents).forEach((F,J)=>{let Q=LY(F,J),w=A.get(Q)||{key:Q,events:[]};w.events.push(F),A.set(Q,w)});for(let F of A.values()){let J=Du(F.events).slice().sort((V,S)=>Ww(S)-Ww(V)),Q=Du(F.events).find((V)=>gr(V)==="control-command-queued")||null,w=J[0]||Q;if(!Q&&!w)continue;let L=String(Q?.sourceNodeId||w?.sourceNodeId||""),U=String(Q?.sourceKind||w?.sourceKind||""),N=Fy(Q)||Fy(w)||C_(Q?.createdAt,w?.createdAt),q=Iu(N),W=String(w?.commandId||Q?.commandId||F.key),z=(gr(w)||"control-command-queued").replace(/^control-command-/u,""),Z="";if(L&&q!==null)Z=`control-source:${W}:${L}`,_.set(W,Z),c({id:Z,runId:f,nodeId:L,procedureRunId:String(Q?.procedureRunId||w?.procedureRunId||""),attempt:"",kind:"control-source",tone:TY(Q||w),status:z,label:`${S_(Q||w)} 发起`,ms:q,timestampIso:N,action:xi(Q||w),sourceKind:U,sourceNodeId:L,targetNodeId:String(w?.targetNodeId||Q?.targetNodeId||""),commandId:W,raw:Q||w},i);let H=w||Q,E=Fy(H)||N,D=Iu(E);if(D===null)continue;let h=KY(H);for(let V of h){let S=EY(r,V,D,H),p=`control-target:${W}:${V}`;if(c({id:p,runId:f,nodeId:V,procedureRunId:S.procedureRunId,attempt:"",kind:"control-target",tone:zY(H),status:z,label:GY(H,V),ms:S.ms,eventMs:D,onInterval:S.onInterval,snapReason:S.snapReason,snapped:Number(S.ms)!==D,timestampIso:E,renderedTimestampIso:p_(Number(S.ms)),action:xi(H),sourceKind:U,sourceNodeId:L,targetNodeId:V,commandId:W,raw:H},i),Z&&L&&L!==V)y.push({id:`control-arrow:${W}:${L}:${V}`,commandId:W,sourceNodeId:L,targetNodeId:V,sourceMarkerId:Z,targetMarkerId:p,sourceKind:U,action:xi(H),status:z})}}let j=[...n,...i].sort((F,J)=>Number(F.ms)-Number(J.ms)||String(F.nodeId).localeCompare(String(J.nodeId))||String(F.id).localeCompare(String(J.id)));return{...NY(j,y),sourceMarkerByCommand:_}}function OY({details:u,selectedNodeId:l,selectedNodeRuntime:f,control:r,onRaw:n}){if(!u)return G("span",{className:"muted"},"点击“抓取过程”读取 node 运行材料;主界面只显示结构化摘要,完整内容需点开原始 JSON。");let i=Du(u.procedureRuns),y=i.at(-1)||{},t=Du(y.attempts),_=t.at(-1)||{},c=Du(y.workerLogTail),A=Du(_.controlEventsTail),j=Du(_.controlPromptsTail),F=Du(_.monitorPromptsTail),J=K9(A),Q=K9(j),w=K9(F),L=_.opencodeMessages||{};return G("div",{className:"pipeline-evidence-list compact"},G(Hr,{title:"Node runtime",subtitle:l||"--",facts:[`status ${f?.status||"pending"}`,`attempts ${f?.attempts??t.length}`,`procedure ${f?.currentProcedureRunId||Vr(y)||"--"}`,r.fetchedAt?`fetched ${tl(r.fetchedAt)}`:"not fetched"],data:u.node||u,onRaw:n,testId:"raw-pipeline-node-runtime"}),G(Hr,{title:"Procedure runs",subtitle:`${i.length} groups`,facts:[`latest ${y.status?.status||y.status||"--"}`,`steps ${Du(y.recentSteps).length}`,`duration ${Br(Iu(y.finishedAt)&&Iu(y.startedAt)?Number(Iu(y.finishedAt))-Number(Iu(y.startedAt)):y.durationMs)}`],data:i,onRaw:n,testId:"raw-pipeline-node-procedures"}),G(Hr,{title:"OpenCode messages",subtitle:String(L.exists?"available":"not indexed"),facts:[`messages ${N6(L.messageCount)}`,`size ${N6(L.size)}`,`updated ${qu(L.updatedAt)}`],data:L,onRaw:n,testId:"raw-pipeline-node-messages"}),G(Hr,{title:"Control prompts",subtitle:"manual / monitor append queues",facts:[`manual tail ${Q.total}`,`monitor tail ${w.total}`,`last ${qu(p9(Q.lastAt,w.lastAt))}`],data:{controlPromptsTail:j,monitorPromptsTail:F},onRaw:n,testId:"raw-pipeline-node-prompts"}),G(Hr,{title:"Control events",subtitle:J.eventKinds.length>0?J.eventKinds.join(", "):"event tail",facts:[`tail ${J.total}`,`parsed ${J.parsed}`,`last ${qu(J.lastAt)}`],data:A,onRaw:n,testId:"raw-pipeline-node-events"}),G(Hr,{title:"Worker log",subtitle:"tail is hidden on main canvas",facts:[`tail ${c.length} lines`,"raw only via button",`procedure ${Vr(y)||"--"}`],data:c,onRaw:n,testId:"raw-pipeline-node-worker-log"}))}function HY({activeRun:u,onRaw:l}){if(!u)return G(Qr,{title:"暂无运行材料",text:"没有 Pipeline epoch 时不会展示运行材料索引。"});let f=Du(u.nodes),r=Du(u.procedureRuns),n=Du(u.submissions),i=Du(u.workerLogTail),y=nw(f),t=nw(r),_=r.filter((A)=>String(A?.status||"").toLowerCase()==="failed"),c=p9(...r.flatMap((A)=>[A.updatedAt,A.finishedAt,A.startedAt]));return G("div",{className:"pipeline-evidence-list"},G(Hr,{title:"Epoch overview",subtitle:u.runId||"--",facts:[`pipeline ${u.pipelineId||"--"}`,`status ${u.status||"--"}`,`started ${qu(u.startedAt)}`,`updated ${qu(u.updatedAt)}`],data:u,onRaw:l,testId:"raw-pipeline-run"}),G(Hr,{title:"Node states",subtitle:`${f.length} nodes`,facts:[`running ${y.running||0}`,`succeeded ${y.succeeded||0}`,`failed ${y.failed||0}`,`pending ${y.pending||0}`],data:f,onRaw:l,testId:"raw-pipeline-run-nodes"}),G(Hr,{title:"Procedure run index",subtitle:`${r.length} procedure records`,facts:[`succeeded ${t.succeeded||0}`,`failed ${t.failed||0}`,`latest ${qu(c)}`,`errors ${_.length}`],data:r,onRaw:l,testId:"raw-pipeline-run-procedures"}),G(Hr,{title:"OA submissions",subtitle:`${n.length} submission files`,facts:[`records ${n.length}`,`task ${N6(u.task)}`,"raw grouped by run"],data:n,onRaw:l,testId:"raw-pipeline-run-submissions"}),G(Hr,{title:"Worker log tail",subtitle:"hidden from main interface",facts:[`tail ${i.length} lines`,"display raw only after click",`updated ${qu(u.updatedAt)}`],data:i,onRaw:l,testId:"raw-pipeline-run-worker-log"}))}function BY({diagnostics:u,onRaw:l}){let f=Du(u?.runs).filter(Xu),r=Du(u?.forbiddenResiduals),n=Xu(u?.guarantees)?u.guarantees:{},i=u?.hasNeutralNodeFinishedEvidence===!0&&u?.hasNoAuditPolicyEvidence===!0&&u?.hasAuditPolicyEvidence===!0,y=u?.ok===!0&&i&&r.length===0,t=f[0]||null,_=[{label:"中性完成事实",ok:n.neutralNodeFinished===!0,hint:"node-finished 不携带流程策略"},{label:"Config 策略判定",ok:n.auditPolicyFromConfig===!0,hint:"OA backend 读取当前 epoch 配置"},{label:"控制命令来自 OA",ok:n.runnerConsumesControlCommandsFromOaEvents===!0,hint:"runner 只消费 OA control.command"},{label:"无独立审核事件",ok:n.noIndependentAuditRequestEvent===!0,hint:"审核由 node-finished + policy 派生"},{label:"无批次门禁",ok:n.noBatchFinishedControlGate===!0,hint:"下游启动由每个 node 完成驱动"}];return G("div",{className:"pipeline-oa-panel","data-testid":"pipeline-oa-event-flow-panel"},G("div",{className:"metric-grid compact"},G(pf,{label:"OA Flow",value:y?"100%":"--",hint:String(u?.mode||"waiting diagnostics"),tone:y?"ok":"warn"}),G(pf,{label:"禁止残留",value:r.length,hint:r.length===0?"source scan clean":"needs cleanup",tone:r.length===0?"ok":"warn"}),G(pf,{label:"No-audit",value:u?.hasNoAuditPolicyEvidence?"OK":"--",hint:"OA 下游策略证据",tone:u?.hasNoAuditPolicyEvidence?"ok":"warn"}),G(pf,{label:"Monitor 审核",value:u?.hasAuditPolicyEvidence?"OK":"--",hint:"OA 控制事件闭环",tone:u?.hasAuditPolicyEvidence?"ok":"warn"})),G("div",{className:"pipeline-oa-guarantees"},_.map((c)=>G("article",{key:c.label,className:`pipeline-oa-guarantee ${c.ok?"ok":"warn"}`},G(bn,{status:c.ok?"online":"warn"},c.ok?"OK":"MISS"),G("div",null,G("strong",null,c.label),G("span",null,c.hint))))),G("div",{className:"pipeline-evidence-list compact"},f.slice(0,6).map((c)=>G(Hr,{key:c.runId,title:String(c.runId||"--"),subtitle:[Number(c.monitorAuditNodeFinishedCount||0)>0?"monitor audit":"",Number(c.noAuditPolicyCount||0)>0?"no-audit policy":""].filter(Boolean).join(" / ")||"event evidence",facts:[`events ${c.eventCount||0}`,`node-finished ${c.nodeFinishedCount||0}`,`policy-in-detail ${c.nodeFinishedWithPolicyCount||0}`,`queued ${c.controlQueuedCount||0}`,`applied ${c.controlAppliedCount||0}`],data:c,onRaw:l,testId:`raw-pipeline-oa-run-${String(c.runId||"run").replace(/[^a-zA-Z0-9_.-]+/g,"-")}`}))),t?G("p",{className:"muted paragraph"},`最新证据 ${t.runId}: ${t.nodeFinishedCount||0} 个 node-finished,${t.controlAppliedCount||0} 个控制结果。`):G(Qr,{title:"暂无 OA 事件流证据",text:"等待 Pipeline backend 暴露 diagnostics。"}),u?G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline OA Event Flow Diagnostics",data:u,onOpen:l,testId:"raw-pipeline-oa-event-flow"})):null)}function VY({quota:u,onRaw:l}){let f=Xu(u?.summary)?u.summary:{},r=Xu(u?.target)?u.target:{},n=Xu(u?.cache)?u.cache:{},i=u?.ok===!0,y=String(u?.modelId||f.modelName||r.modelName||"MiniMax-M2.7"),t=f.totalCount??r.currentIntervalTotalCount,_=f.usageCount??r.currentIntervalUsageCount,c=f.remainingCount??r.currentIntervalRemainingCount,A=f.remainingRatio??(Number.isFinite(Number(t))&&Number(t)>0&&Number.isFinite(Number(c))?Number(c)/Number(t):void 0),j=f.usageRatio??(Number.isFinite(Number(t))&&Number(t)>0&&Number.isFinite(Number(_))?Number(_)/Number(t):void 0),F=f.resetAt||r.endAt,J=f.remainsMs??r.remainsMs,Q=Number(c),w=!i||Number.isFinite(Q)&&Q<=0?"warn":"ok",L=[i?`endpoint ${u?.endpoint||"--"}`:"quota unavailable",`fetched ${Q6(u?.fetchedAt)}`,n.hit?`cache ${Br(n.ageMs)}`:"live quota"];return G("div",{className:"pipeline-minimax-quota-panel","data-testid":"pipeline-minimax-quota-panel"},G("div",{className:"metric-grid compact"},G(pf,{label:"MiniMax",value:i?y:"--",hint:u?.modelComponent||u?.error||"model/minimax-m27",tone:w}),G(pf,{label:"当前窗口",value:`${L9(_)}/${L9(t)}`,hint:`已用 ${rw(j)}`,tone:w}),G(pf,{label:"剩余额度",value:L9(c),hint:`剩余 ${rw(A)}`,tone:w}),G(pf,{label:"重置时间",value:Q6(F),hint:J!==void 0?`约 ${Br(J)}`:qu(F),tone:w})),G(P9,{items:L}),i?G("p",{className:"muted paragraph"},`MiniMax 限额来自 D601 Pipeline 后端实时查询;当前模型匹配 ${f.modelName||r.modelName||y}。`):G(il,{error:u?.error||"MiniMax 限额查询失败"}),u?G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline MiniMax Quota",data:u,onOpen:l,testId:"raw-pipeline-minimax-quota"})):null)}function DY({epochs:u,activeRun:l,activePipeline:f,pipelineNodes:r,pipelineEdges:n,runDetails:i,nodeDetails:y,nodeDetailsState:t,ganttScale:_=P_,onGanttScaleChange:c,onRunChange:A,onIntervalSelect:j,onMarkerSelect:F,selection:J,detailOpen:Q,onDetailOpenChange:w,onRaw:L}){let[U,N]=af($Y),[q,W]=af({startY:0,endY:0,startMs:0,endMs:0}),[z,Z]=af(Date.now()),H=xn(null),E=String(l?.runId||""),D=Boolean(Q),h=(Au)=>{if(typeof w==="function")w(Au)},V=vn(_??P_),S=String(i?.runId||"")===E?i?.details:null,p=S?{...Xu(l)?l:{},...Xu(S)?S:{},runId:E,procedureRuns:Du(S?.procedureRuns).length>0?S.procedureRuns:l?.procedureRuns}:l,O=Sw(p,r,z),m=S?ZY(S,p):{markers:[],arrows:[]},X=Du(m.markers),v=_Y(p,O,X),T=UY(v,V,O,z),Y=String(T.source||"frontend-y"),k=O.map((Au)=>JY(Au,T,z)),I={startMs:Number(T.startMs),endMs:Number(T.endMs),durationMs:Math.max(1,Number(T.durationMs??Number(T.endMs)-Number(T.startMs)))},b=h9(V),o={...b,pxPerMinute:Number(T.pxPerMinute??b.pxPerMinute)},g=Math.round(Number(T.chartHeight||360)),x=O.some(b9);U0(()=>{if(!E||!x)return;let Au=window.setInterval(()=>Z(Date.now()),1000);return()=>window.clearInterval(Au)},[E,x]);let lu=dS(f,r,Array.isArray(n)?n:[]),_u=r.map((Au)=>String(Au?.id||"")).filter(Boolean),$u=k.map((Au)=>String(Au.nodeId||"")).filter(Boolean),ju=X.map((Au)=>String(Au.nodeId||"")).filter(Boolean),zu=Array.from(new Set([...lu,..._u,...$u,...ju])),Wu={startY:0,endY:g,startMs:Number(I.startMs),endMs:Number(I.endMs)},P=Number(q?.endY||0)>0?q:Wu,e=(Au)=>{return G6(Au,I,g,T)<=Number(P.endY)&&Y9(Au,I,g,T)>=Number(P.startY)},uu=(Au)=>{let su=Jr(Au,I,g,T);return su>=Number(P.startY)&&su<=Number(P.endY)},Ku=new Set(zu.filter((Au)=>k.some((su)=>su.nodeId===Au&&e(su))||X.some((su)=>su.nodeId===Au&&uu(su)))),s=U?zu.filter((Au)=>Ku.has(Au)):zu,Nu=`${W9}px ${s.length>0?s.map(()=>`${j0}px`).join(" "):"minmax(160px, 1fr)"}`,Eu=Du(T.ticks).filter(Xu),Hu=String(J?.mode==="interval"?J?.interval?.procedureRunId||"":""),vu=String(J?.mode==="event"?J?.marker?.id||"":""),ul=()=>{let Au=H.current;if(!Au){W(Wu);return}let su=Math.max(0,Au.scrollTop-w9),Jf=Math.max(120,Au.clientHeight-w9),pu=Math.min(g,su+Jf),ff={startY:su,endY:pu,startMs:Number(I.startMs),endMs:Number(I.endMs)},rf=Math.max(0,Math.min(1,su/g)),ur=Math.max(rf,Math.min(1,pu/g)),nf=Math.max(1,Number(I.endMs)-Number(I.startMs));ff.startMs=Number(I.startMs)+nf*rf,ff.endMs=Number(I.startMs)+nf*ur,W(ff)};U0(()=>{let Au=H.current,su=window.setTimeout(ul,0);return Au?.addEventListener("scroll",ul),window.addEventListener("resize",ul),()=>{window.clearTimeout(su),Au?.removeEventListener("scroll",ul),window.removeEventListener("resize",ul)}},[E,I.startMs,I.endMs,g]);let mu=Math.max(0,zu.length-s.length),Fl=new Set(X.filter((Au)=>s.includes(String(Au.nodeId||""))&&uu(Au)).map((Au)=>String(Au.id))),Uf=new Map(X.map((Au)=>[String(Au.id),Au])),Ef=Du(m.arrows).filter((Au)=>{if(!Fl.has(String(Au.targetMarkerId||"")))return!1;if(String(Au.action||"")==="observe")return s.includes(String(Au.sourceNodeId||""));return Fl.has(String(Au.sourceMarkerId||""))}),lf=W9+Math.max(1,s.length)*j0,ol=(Au)=>{let su=vn(Au.target.value);if(typeof c==="function")c(su);window.setTimeout(ul,0)},Zf=()=>iY({title:`${f?.id||"pipeline"}-${E||"epoch"}-gantt`,meta:[`run ${E||"--"}`,`${qu(I.startMs)} -> ${qu(I.endMs)}`,`duration ${Br(I.durationMs)}`,`${o.label} / ${E9(o.pxPerMinute)} px/min`,`${s.length}/${zu.length} nodes`,`${X.length} markers`],visibleNodeIds:s,intervals:k,markers:X.filter((Au)=>s.includes(String(Au.nodeId||""))),arrows:Ef,ticks:Eu,bounds:I,chartHeight:g,backendLayout:T}),mf=Xu(S?.gantt?.diagnostics)?S.gantt.diagnostics:null;return G(F0,{title:"Epoch 甘特图",eyebrow:`${f?.id||"pipeline"} / ${u.length} epochs`,className:"pipeline-wide-panel",loading:i?.loading,actions:G("div",{className:"pipeline-gantt-actions"},G("select",{value:E,disabled:u.length===0,onChange:(Au)=>A(Au.target.value),"data-testid":"pipeline-epoch-select"},u.map((Au)=>G("option",{key:Au.runId,value:Au.runId},X9(u,Au)))),G("label",{className:"pipeline-gantt-toggle"},G("input",{type:"checkbox","data-testid":"pipeline-gantt-auto-hide-idle",checked:U,onChange:(Au)=>{N(Boolean(Au.target.checked)),window.setTimeout(ul,0)}}),G("span",null,"自动隐藏空闲列")),G("label",{className:"pipeline-gantt-scale"},G("span",null,G("b",null,"时间尺度"),G("em",{"data-testid":"pipeline-gantt-scale-label"},`${o.label} · ${E9(o.pxPerMinute)} px/min`)),G("input",{type:"range",min:0,max:100,step:0.01,value:V,onChange:ol,"aria-label":"调整甘特图时间尺度","data-testid":"pipeline-gantt-time-scale"}),G("small",null,G("span",null,"全局"),G("span",null,"细节"))),l?G("button",{type:"button",className:"ghost-btn",onClick:Zf,disabled:s.length===0,"data-testid":"pipeline-export-gantt"},"导出甘特图"):null,l?G(sr,{title:`Pipeline Epoch ${l.runId}`,data:l,onOpen:L,testId:"raw-pipeline-epoch-gantt"}):null)},!l?G(Qr,{title:"暂无 Epoch",text:"当前 pipeline 还没有完整运行记录。"}):k.length===0?G(Qr,{title:"暂无时间区间",text:"等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。"}):G("div",{className:"pipeline-gantt-wrap"},G("div",{className:`pipeline-gantt-detail-layout ${D?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-gantt-detail-layout","data-sidebar-open":D?"true":"false"},G("div",{className:"pipeline-gantt-main"},G("div",{className:"pipeline-gantt-main-head"},G("div",{className:"pipeline-gantt-meta"},G("span",null,`time ${qu(I.startMs)} -> ${qu(I.endMs)}`),G("span",null,`duration ${Br(I.durationMs)}`),G("span",null,`scale ${o.label} / ${E9(o.pxPerMinute)} px/min`),G("span",null,`layout ${Y}`),mf?G("span",null,`align ${mf.timeAxisAlignmentOk===!1?"check":"ok"}`):null,G("span",null,`visible ${s.length}/${zu.length} nodes`),S?G("span",null,`markers ${X.length}`):null,U&&mu>0?G("span",null,`hidden idle ${mu}`):null),!D?G("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!J?.mode,onClick:()=>h(!0),"data-testid":"pipeline-gantt-sidebar-toggle"},J?.mode?"展开详情":"点击甘特图元素展开详情"):null),G("div",{className:"pipeline-gantt-viewport",ref:H,"data-testid":"pipeline-epoch-gantt","data-pipeline-id":f?.id||"","data-run-id":E,"data-layout-source":Y,"data-start-ms":String(I.startMs),"data-end-ms":String(I.endMs),"data-chart-height":String(g)},G("div",{className:"pipeline-gantt-board",style:{gridTemplateColumns:Nu,minWidth:`${lf}px`}},G("div",{className:"pipeline-gantt-head time"},"Time"),s.length===0?G("div",{className:"pipeline-gantt-head empty"},"当前时间窗无工作节点"):s.map((Au)=>G("div",{key:`head-${Au}`,className:"pipeline-gantt-head node",title:Au,"data-testid":"pipeline-gantt-head-node","data-node-id":Au},G(XS,{value:Au}))),G("div",{className:"pipeline-gantt-time-axis",style:{height:`${g}px`}},Eu.map((Au)=>{let su=Mw(Au,I,g,T);return G("div",{key:`tick-${Au.ms}-${su}`,className:"pipeline-gantt-tick",style:{top:`${su}px`},"data-testid":"pipeline-gantt-tick","data-ms":String(Au.ms),"data-y":String(su)},G("b",null,qu(Au.ms)),G("span",null,`+${Br(Number(Au.offsetMs??Number(Au.ms)-Number(I.startMs)))}`))})),s.length>0?G("svg",{className:"pipeline-gantt-arrow-layer",width:s.length*j0,height:g,viewBox:`0 0 ${s.length*j0} ${g}`,style:{left:`${W9}px`,top:`${w9}px`,width:`${s.length*j0}px`,height:`${g}px`},"aria-hidden":"true"},G("defs",null,G("marker",{id:"pipeline-gantt-arrowhead",viewBox:"0 0 10 10",refX:9,refY:5,markerWidth:6,markerHeight:6,orient:"auto-start-reverse"},G("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"context-stroke"}))),Ef.map((Au)=>{let su=Uf.get(String(Au.targetMarkerId||""));if(!su)return null;let Jf=Uf.get(String(Au.sourceMarkerId||"")),pu=String(Jf?.nodeId||Au.sourceNodeId||""),ff=s.indexOf(pu),rf=s.indexOf(String(su.nodeId||""));if(ff<0||rf<0)return null;let ur=ff*j0+j0/2,nf=rf*j0+j0/2,Of=Jf?Jr(Jf,I,g,T):Jr(su,I,g,T),N0=Jr(su,I,g,T);return G("path",{key:Au.id,className:`pipeline-gantt-arrow ${String(Au.sourceKind||"").toLowerCase()} ${String(Au.status||"").toLowerCase()} ${String(Au.action||"").toLowerCase()}`,d:Rw(ur,Of,nf,N0),markerEnd:"url(#pipeline-gantt-arrowhead)","data-testid":String(Au.action||"")==="observe"?"pipeline-gantt-observation-arrow":"pipeline-gantt-arrow","data-source-node-id":String(Au.sourceNodeId||""),"data-target-node-id":String(Au.targetNodeId||""),"data-target-marker-id":String(Au.targetMarkerId||""),"data-action":String(Au.action||""),"data-source-y":String(Of),"data-target-y":String(N0)})})):null,s.length===0?G("div",{className:"pipeline-gantt-empty-col",style:{height:`${g}px`}},"滚动到有活动的时间段后,相关 node 列会自动出现。"):s.map((Au)=>{let su=k.filter((pu)=>pu.nodeId===Au),Jf=X.filter((pu)=>String(pu.nodeId||"")===Au);return G("div",{key:`col-${Au}`,className:"pipeline-gantt-node-col",style:{height:`${g}px`}},su.map((pu)=>{let ff=G6(pu,I,g,T),rf=Y9(pu,I,g,T),ur=Cw(pu,I,g,T),nf=String(pu.procedureRunId||`${Au}-${pu.startMs}`);return G("button",{key:nf,type:"button",className:`pipeline-gantt-bar ${pu.status} ${pu.live?"live":""} ${Hu===nf?"selected":""}`,style:{top:`${ff}px`,height:`${ur}px`},title:`${Au} ${pu.status} ${qu(pu.startedAt||pu.startMs)} -> ${qu(pu.finishedAt||pu.endMs)}`,onClick:()=>j(pu),"data-testid":"pipeline-gantt-line","data-node-id":Au,"data-procedure-run-id":String(pu.procedureRunId||""),"data-status":String(pu.status||""),"data-live":pu.live?"true":"false","data-start-ms":String(pu.startMs||""),"data-end-ms":String(pu.endMs||""),"data-y1":String(ff),"data-y2":String(rf),"data-natural-height":String(Math.max(0,rf-ff))},G("strong",null,pu.status||"working"),G("span",null,Br(pu.durationMs)))}),Jf.map((pu)=>G("button",{key:pu.id,type:"button",className:`pipeline-gantt-marker ${pu.kind} ${pu.tone||""} ${pu.status||""} ${vu===String(pu.id)?"selected":""}`,style:{top:`${Jr(pu,I,g,T)}px`},title:`${pu.label||"event"} / ${qu(pu.timestampIso||pu.timestamp||pu.ms)}`,onClick:()=>F(pu),"data-testid":pu.kind==="prompt"?"pipeline-gantt-prompt-marker":"pipeline-gantt-control-marker","data-marker-id":String(pu.id||""),"data-ms":String(pu.ms??pu.eventMs??""),"data-y":String(Jr(pu,I,g,T))})))})))),D?G(DS,{selection:J,runDetails:i,nodeDetails:y,nodeDetailsState:t,onRaw:L,onCollapse:()=>h(!1)}):null)))}function h0(){return{loading:!1,actionLoading:"",error:"",message:"",details:null,fetchedAt:null,appendPrompt:"",guidePrompt:"",modifyPrompt:"",approveReason:"",redoReason:""}}function Rn(){return{mode:"",runId:"",interval:null,marker:null}}function Z9(){return{runId:"",loading:!1,error:"",details:null,fetchedAt:null}}function V_(u,l){return`${u}/microservices/pipeline/proxy${l}`}function XY({activeRun:u,pipelineRuns:l,selectedRunId:f,onRunChange:r,selectedNodeId:n,selectedNodeConfig:i,selectedNodeRuntime:y,control:t,onControlChange:_,onFetch:c,onAction:A,onRaw:j,onCollapse:F}){let J=String(u?.runId||""),Q=String(y?.status||"pending"),w=!J||!n||t.loading||Boolean(t.actionLoading),L=(N)=>(q)=>_({[N]:q.target.value,error:"",message:""}),U=l.length>0?l:u?[u]:[];return G("aside",{className:"pipeline-node-control","data-testid":"pipeline-node-control"},G("div",{className:"pipeline-node-control-head"},G("div",null,G("p",{className:"panel-eyebrow"},"Manual Node Control"),G(nl,{title:n||"点击控制图中的 node",level:3,loading:t.loading||Boolean(t.actionLoading)})),G("div",{className:"pipeline-node-control-head-actions"},n?G(bn,{status:Q},Q):G(bn,{status:"pending"},"idle"),G("button",{type:"button",className:"ghost-btn mini",onClick:F,"data-testid":"pipeline-node-sidebar-collapse"},"收起"))),G("div",{className:"pipeline-control-runbar"},G("label",null,G("span",null,"目标 run"),G("select",{value:J||f,disabled:U.length===0,onChange:(N)=>r(N.target.value),"data-testid":"pipeline-node-run-select"},U.map((N)=>G("option",{key:N.runId,value:N.runId},`${N.runId||"--"} / ${N.status||"--"}`)))),G("button",{type:"button",className:"ghost-btn",disabled:w,onClick:c,"data-testid":"pipeline-node-fetch"},t.loading?"抓取中":"抓取过程"),t.details?G(sr,{title:`Pipeline Node ${n}`,data:t.details,onOpen:j,testId:"raw-pipeline-node-control"}):null),G("div",{className:"pipeline-control-meta"},G("span",null,G("b",null,"kind"),String(i?.kind||"--")),G("span",null,G("b",null,"procedure"),String(y?.currentProcedureRunId||"--")),G("span",null,G("b",null,"attempts"),String(y?.attempts??"--")),G("span",null,G("b",null,"updated"),qu(u?.updatedAt))),!n?G(Qr,{title:"未选择 node",text:"点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。"}):null,G(il,{error:t.error,wide:!0}),G("div",{className:"pipeline-control-actions"},G("label",null,G("span",null,"实时追加 prompt(仅 running node)"),G("textarea",{value:t.appendPrompt,onChange:L("appendPrompt"),placeholder:"让当前执行中的 agent 继续、补充检查或调整当前步骤...",rows:4,disabled:!n,"data-testid":"pipeline-node-append-input"}),G("button",{type:"button",className:"primary-btn compact",disabled:w||!String(t.appendPrompt||"").trim(),onClick:()=>A("append"),"data-testid":"pipeline-node-append-button"},t.actionLoading==="append"?"追加中":"追加到运行中 node")),G("label",null,G("span",null,"下次尝试引导 prompt"),G("textarea",{value:t.guidePrompt,onChange:L("guidePrompt"),placeholder:"给该 node 下一次 attempt 的执行提示;不会立即打断当前 session。",rows:4,disabled:!n,"data-testid":"pipeline-node-guide-input"}),G("button",{type:"button",className:"ghost-btn compact",disabled:w||!String(t.guidePrompt||"").trim(),onClick:()=>A("guide"),"data-testid":"pipeline-node-guide-button"},t.actionLoading==="guide"?"下发中":"下发 guide")),G("label",null,G("span",null,"完成后增量修改 prompt"),G("textarea",{value:t.modifyPrompt,onChange:L("modifyPrompt"),placeholder:"在该 node 已完成结果基础上追加修改要求;runner 会重跑目标 node,并保留同 node 既有 OA 输出作为上下文。",rows:4,disabled:!n,"data-testid":"pipeline-node-modify-input"}),G("button",{type:"button",className:"ghost-btn compact",disabled:w||!String(t.modifyPrompt||"").trim(),onClick:()=>A("modify"),"data-testid":"pipeline-node-modify-button"},t.actionLoading==="modify"?"排队中":"增量修改 node")),G("label",null,G("span",null,"Monitor 审核通过原因"),G("textarea",{value:t.approveReason,onChange:L("approveReason"),placeholder:"当流程配置开启 monitor 审核时,记录审核通过原因并释放后续 node。",rows:3,disabled:!n,"data-testid":"pipeline-node-approve-input"}),G("button",{type:"button",className:"primary-btn compact",disabled:w||!String(t.approveReason||"").trim(),onClick:()=>A("approve"),"data-testid":"pipeline-node-approve-button"},t.actionLoading==="approve"?"提交中":"审核通过")),G("label",null,G("span",null,"重做 / restart 原因"),G("textarea",{value:t.redoReason,onChange:L("redoReason"),placeholder:"说明为什么需要重做;runner 会重置目标 node 以及非 rework 下游 node。",rows:4,disabled:!n,"data-testid":"pipeline-node-redo-input"}),G("button",{type:"button",className:"danger-btn compact",disabled:w||!String(t.redoReason||"").trim(),onClick:()=>A("redo"),"data-testid":"pipeline-node-redo-button"},t.actionLoading==="redo"?"排队中":"重做 node"))),G("div",{className:"pipeline-control-evidence"},G("strong",null,"Node 过程索引"),G(OY,{details:t.details,selectedNodeId:n,selectedNodeRuntime:y,control:t,onRaw:j})))}function xw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((fu)=>fu.id==="pipeline")||null,[n,i]=af({loading:!1,error:"",health:null,snapshot:null,oaDiagnostics:null,minimaxQuota:null,refreshedAt:null}),[y,t]=af(""),[_,c]=af(""),[A,j]=af(""),[F,J]=af(h0()),[Q,w]=af({}),[L,U]=af(Rn()),[N,q]=af(Z9()),[W,z]=af(P_),[Z,H]=af(!1),[E,D]=af(!1),h=xn(0),{addNotification:V}=Xf(),S=xn(!1),p=xn(0),O=xn(""),m=xn({}),X=xn(""),v=xn("");async function T(fu={}){let Bu=fu.silent===!0;if(!r)return;if(S.current)return;S.current=!0;let Yu=h.current+1;if(h.current=Yu,!Bu)i((au)=>({...au,loading:!0,error:""}));try{let au=`__unideskArrayLimit=registry.components:80,runs:${JS}`,[_l,Pl,yl]=await Promise.all([Mn(`${f}/microservices/pipeline/proxy/api/snapshot?${au}`,{cache:"no-store"}),Mn(`${f}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`,{cache:"no-store"}).catch((Xr)=>({ok:!1,error:Ou(Xr,"OA event flow diagnostics failed")})),Mn(`${f}/microservices/pipeline/proxy/api/model-quota/minimax`,{cache:"no-store"}).catch((Xr)=>({ok:!1,error:Ou(Xr,"MiniMax quota failed")}))]);if(Yu!==h.current)return;let Qf={ok:_l?.ok!==!1,service:"pipeline-v2-control snapshot"};i({loading:!1,error:"",health:Qf,snapshot:_l,oaDiagnostics:Pl,minimaxQuota:yl,refreshedAt:new Date})}catch(au){if(Yu!==h.current)return;i((_l)=>({..._l,loading:!1,error:Ou(au,"Pipeline 加载失败")}))}finally{S.current=!1}}U0(()=>{if(T(),!r)return;let fu=()=>{if(A6())T({silent:!0})},Bu=window.setInterval(()=>{fu()},uw),Yu=()=>{if(A6())fu()};return document.addEventListener("visibilitychange",Yu),()=>{window.clearInterval(Bu),document.removeEventListener("visibilitychange",Yu)}},[r?.id,r?.runtime?.providerStatus,f]);let Y=SS(r),k=pS(r),I=YS(r),b=n.snapshot||{},o=n.oaDiagnostics||null,g=n.minimaxQuota||null,{components:x,pipelines:lu,runs:_u}=mS(b),$u=String(_u[0]?.pipelineId||""),ju=($u?lu.find((fu)=>String(fu.id||"")===$u):null)||lu[0]||{},zu=lu.find((fu)=>String(fu.id||"")===y)||ju,Wu=String(zu.id||""),P=Hw(zu),e=M9(zu),uu=Qw(_u,Wu),Ku=tY(_u,Wu),s=Ku.find((fu)=>String(fu?.runId||"")===_)||uu,Nu=String(N.runId||"")===String(s?.runId||"")?RS(N.details):null,Eu=xS(s,Nu),Hu=String(Eu?.runId||""),vu=P.find((fu)=>String(fu?.id||"")===A)||null,ul=A?Bw(Eu,A):null,mu=CS(_u),Fl=kS(x),Uf=Number(n.health?.components)||$w(b,"registry.components",x.length),Ef=$w(b,"runs",_u.length),lf=jw(zu,Eu,x),ol={nodes:lf.nodes.map((fu)=>fu.id===A?{...fu,selected:!0,className:`${fu.className||""} selected-control-node`}:fu),edges:lf.edges},Zf=lu.map((fu)=>{let Bu=String(fu.id||"pipeline"),Yu=Qw(_u,Bu);return{title:`${Bu}-${Yu?.runId||"snapshot"}`,flow:jw(fu,Yu,x)}}),mf=String(L?.runId||Hu||""),Au=String(L?.interval?.nodeId||L?.marker?.nodeId||""),su=mf&&Au?Q[T9(mf,Au)]||null:null,Jf=q6(F.details,mf,Au),pu=q6(su?.details,mf,Au)||Jf,ff=mf&&Au?{...Xu(su)?su:{},runId:mf,nodeId:Au,details:pu,loading:Boolean(su?.loading)||!pu&&Boolean(F.loading)&&A===Au,error:String(su?.error||""),fetchedAt:su?.fetchedAt||(Jf?F.fetchedAt:null)}:null,rf=Ku.map((fu)=>String(fu?.runId||"")).filter(Boolean).join("|"),ur=P.map((fu)=>String(fu?.id||"")).filter(Boolean).join("|");U0(()=>{X.current=A},[A]),U0(()=>{v.current=Hu},[Hu]),U0(()=>{if(!_||rf.split("|").includes(_))return;c("")},[_,rf]),U0(()=>{if(!A||ur.split("|").includes(A))return;j(""),J(h0()),U(Rn()),H(!1),D(!1)},[A,ur]),U0(()=>{if(!A)H(!1)},[A]),U0(()=>{if(!L.mode)D(!1)},[L.mode]);async function nf(fu=Hu,Bu={}){if(!fu){q(Z9());return}let Yu=vn(Bu.scale??W??P_),au=`${fu}:timeline`;if(O.current===au)return;O.current=au;let _l=Bu.silent===!0,Pl=p.current+1;p.current=Pl,q((yl)=>({runId:fu,scale:Yu,loading:!_l||String(yl.runId||"")!==fu||!yl.details,error:"",details:_l&&yl.runId===fu?yl.details:yl.runId===fu?yl.details:null,fetchedAt:yl.runId===fu?yl.fetchedAt:null}));try{let[yl,Qf]=await Promise.all([Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}?tail=160&view=timeline`),{cache:"no-store",strictJson:!0}),Mn(V_(f,`/api/runs/${encodeURIComponent(fu)}`),{cache:"no-store"}).catch((Xr)=>({ok:!1,runSummaryError:Ou(Xr,"抓取评分失败")}))]);if(Pl!==p.current)return;q({runId:fu,scale:Yu,loading:!1,error:"",details:{...yl,run:Xu(Qf?.run)?Qf.run:void 0,runSummaryError:Qf?.runSummaryError},fetchedAt:new Date})}catch(yl){if(Pl!==p.current)return;q((Qf)=>({runId:fu,scale:Yu,loading:!1,error:Ou(yl,"抓取 epoch 执行过程失败"),details:Qf.runId===fu?Qf.details:null,fetchedAt:Qf.runId===fu?Qf.fetchedAt:null}))}finally{if(O.current===au)O.current=""}}function Of(fu,Bu,Yu){let au=T9(fu,Bu);w((_l)=>{let Pl={..._l,[au]:{...Xu(_l?.[au])?_l[au]:{},runId:fu,nodeId:Bu,...Yu}},yl=Object.keys(Pl);if(yl.length>32)for(let Qf of yl.slice(0,yl.length-32))delete Pl[Qf];return Pl})}async function N0(fu,Bu){if(!fu||!Bu)return;let Yu=T9(fu,Bu),au=Number(m.current?.[Yu]||0)+1;m.current={...m.current,[Yu]:au},Of(fu,Bu,{loading:!0,error:""});try{let _l=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}/nodes/${encodeURIComponent(Bu)}?tail=160`),{cache:"no-store",strictJson:!0});if(Number(m.current?.[Yu]||0)!==au)return;let Pl=new Date;if(Of(fu,Bu,{loading:!1,details:_l,fetchedAt:Pl,error:""}),X.current===Bu&&v.current===fu)J((yl)=>({...yl,loading:!1,details:_l,fetchedAt:Pl,error:""}))}catch(_l){if(Number(m.current?.[Yu]||0)!==au)return;Of(fu,Bu,{loading:!1,error:Ou(_l,"抓取 Gantt node 详情失败")})}}U0(()=>{if(!Hu){q(Z9());return}nf(Hu);let fu=()=>{if(A6())nf(Hu,{silent:!0})},Bu=window.setInterval(()=>{fu()},uw),Yu=()=>{if(A6())fu()};return document.addEventListener("visibilitychange",Yu),()=>{window.clearInterval(Bu),document.removeEventListener("visibilitychange",Yu)}},[Hu,f]);async function lr(fu=Hu,Bu=A){if(!fu||!Bu){J((Yu)=>({...Yu,error:"请先选择 run 和 node",message:""}));return}J((Yu)=>({...Yu,loading:!0,error:"",message:""}));try{let Yu=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}/nodes/${encodeURIComponent(Bu)}?tail=160`),{cache:"no-store",strictJson:!0}),au=new Date;J((_l)=>({..._l,loading:!1,details:Yu,fetchedAt:au,error:""})),Of(fu,Bu,{loading:!1,details:Yu,fetchedAt:au,error:""})}catch(Yu){J((au)=>({...au,loading:!1,error:Ou(Yu,"抓取 node 执行过程失败")}))}}async function k0(fu){let Bu=String(fu?.runId||Hu||""),Yu=String(fu?.nodeId||"");if(U({mode:"interval",runId:Bu,interval:fu,marker:null}),D(!0),!Bu||!Yu)return;if(Bu!==Hu)c(Bu);j(Yu),J(h0()),nf(Bu,{silent:!0}),N0(Bu,Yu)}async function Ul(fu){let Bu=String(fu?.runId||Hu||""),Yu=String(fu?.nodeId||"");if(U({mode:"event",runId:Bu,interval:null,marker:fu}),D(!0),!Bu)return;if(Bu!==Hu)c(Bu);if(nf(Bu,{silent:!0}),!Yu)return;j(Yu),J(h0()),N0(Bu,Yu)}async function bi(fu){if(!Hu||!A){J((au)=>({...au,error:"请先选择 run 和 node",message:""}));return}let Bu=fu==="append"?"prompts":fu,Yu=fu==="append"?F.appendPrompt:fu==="guide"?F.guidePrompt:fu==="modify"?F.modifyPrompt:fu==="approve"?F.approveReason:F.redoReason;if(!String(Yu||"").trim()){J((au)=>({...au,error:"操作内容不能为空",message:""}));return}J((au)=>({...au,actionLoading:fu,error:"",message:""}));try{let au=fu==="redo"||fu==="approve"?{reason:Yu,source:"unidesk-frontend",sourceKind:"webui"}:{prompt:Yu,source:"unidesk-frontend",sourceKind:"webui"},_l=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(Hu)}/nodes/${encodeURIComponent(A)}/${Bu}`),{method:"POST",body:JSON.stringify(au)});if(J((yl)=>({...yl,actionLoading:"",details:_l,fetchedAt:new Date,appendPrompt:fu==="append"?"":yl.appendPrompt,guidePrompt:fu==="guide"?"":yl.guidePrompt,modifyPrompt:fu==="modify"?"":yl.modifyPrompt,approveReason:fu==="approve"?"":yl.approveReason,redoReason:fu==="redo"?"":yl.redoReason,message:fu==="append"?"已追加到运行中 node":fu==="guide"?"已下发 guide,等待 runner 处理":fu==="modify"?"已排队增量修改命令":fu==="approve"?"已提交审核通过决策":"已排队重做命令"})),V("success",fu==="append"?"已追加到运行中 node":fu==="guide"?"已下发 guide,等待 runner 处理":fu==="modify"?"已排队增量修改命令":fu==="approve"?"已提交审核通过决策":"已排队重做命令"),await lr(Hu,A),await nf(Hu,{silent:!0}),fu!=="append")await T()}catch(au){J((_l)=>({..._l,actionLoading:"",error:Ou(au,"node 控制操作失败")}))}}if(!r)return G(Qr,{title:"Pipeline 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=pipeline"});return G("div",{className:"pipeline-page","data-testid":"pipeline-page"},G(F0,{title:"Pipeline v2 工作台",eyebrow:"D601 Snapshot 用户服务",loading:n.loading,actions:G("div",{className:"panel-actions"},G("button",{type:"button",className:"ghost-btn",onClick:T,disabled:n.loading,"data-testid":"pipeline-refresh-button"},n.loading?"刷新中":"刷新"),G(sr,{title:"Pipeline 用户服务",data:r,onOpen:l,testId:"raw-pipeline-service"}))},G("div",{className:"pipeline-hero"},G("div",null,G("div",{className:"node-version-line"},G(bn,{status:Y.providerStatus==="online"?"online":"warn"},Y.providerStatus||"unknown"),G("span",null,r.providerId),G("span",null,I.public?"公网暴露":"仅 UniDesk frontend 代理访问")),G("p",{className:"muted paragraph"},r.description)),G("div",{className:"microservice-ref-card"},G("span",null,"Repo"),G("strong",null,k.url||"--"),G("code",null,k.commitId||"--")),G("div",{className:"microservice-ref-card"},G("span",null,"D601 Docker"),G("strong",null,`${I.nodeBindHost||"--"}:${I.nodePort||"--"}`),G("code",null,`${k.composeFile||"--"} / ${k.composeService||"--"}`))),G(il,{error:n.error,wide:!0})),G("div",{className:"pipeline-grid"},G(F0,{title:"控制图",eyebrow:`${zu.id||"pipeline"} / run ${Eu?.status||"--"}`,className:"pipeline-wide-panel",loading:n.loading,actions:G("div",{className:"pipeline-toolbar"},G("select",{value:Wu,disabled:lu.length===0,onChange:(fu)=>{t(fu.target.value),c(""),j(""),J(h0()),U(Rn()),H(!1),D(!1)},"data-testid":"pipeline-select"},lu.map((fu)=>G("option",{key:fu.id,value:fu.id},fu.id||fu.key))),G("select",{value:Hu,disabled:Ku.length===0,onChange:(fu)=>{if(c(fu.target.value),J(h0()),U(Rn()),H(!1),D(!1),A)lr(fu.target.value,A)},"data-testid":"pipeline-run-select"},Ku.map((fu)=>G("option",{key:fu.runId,value:fu.runId},X9(Ku,fu)))),G("button",{type:"button",className:"ghost-btn",disabled:ol.nodes.length===0,onClick:()=>Xw(ol,`${zu.id||"pipeline"}-${Eu?.runId||"snapshot"}`),"data-testid":"pipeline-export-graph"},"导出渲染图"),G("button",{type:"button",className:"ghost-btn",disabled:Zf.every((fu)=>fu.flow.nodes.length===0),onClick:()=>yY(Zf),"data-testid":"pipeline-export-all-graphs"},"批量导出"))},P.length===0?G(Qr,{title:"暂无控制图",text:"等待 D601 pipeline backend 返回 config.nodes / config.edges"}):G("div",{className:`pipeline-control-shell ${Z?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-control-shell","data-sidebar-open":Z?"true":"false"},G("div",{className:"pipeline-flow-frame","data-testid":"pipeline-react-flow"},G(IW,{nodes:ol.nodes,edges:ol.edges,nodeTypes:wS,edgeTypes:WS,fitView:!0,fitViewOptions:{padding:0.18},nodesDraggable:!1,nodesConnectable:!1,elementsSelectable:!0,minZoom:0.25,maxZoom:1.4,proOptions:{hideAttribution:!0},onNodeClick:(fu,Bu)=>{let Yu=String(Bu.id);if(j(Yu),J(h0()),H(!0),Hu)lr(Hu,Yu)}},G(sW,{gap:22,size:1,color:"rgba(215, 161, 58, 0.24)"}),G(oW,{showInteractive:!1})),!Z?G("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!A,onClick:()=>H(!0),"data-testid":"pipeline-node-sidebar-toggle"},A?"展开 node 控制":"点击 node 展开控制"):null),Z?G(XY,{activeRun:Eu,pipelineRuns:Ku,selectedRunId:_,onRunChange:(fu)=>{if(c(fu),J(h0()),U(Rn()),A)lr(fu,A)},selectedNodeId:A,selectedNodeConfig:vu,selectedNodeRuntime:ul,control:F,onControlChange:(fu)=>J((Bu)=>({...Bu,...fu})),onFetch:()=>lr(),onAction:bi,onRaw:l,onCollapse:()=>H(!1)}):null),G("div",{className:"pipeline-flow-summary"},G("span",null,`${ol.nodes.length} nodes`),G("span",null,`${ol.edges.length} edges`),G("span",null,`${lu.length} pipelines`),G("span",null,`source config+components(${x.length})`),G("span",null,`run ${Eu?.runId||"--"}`),G("span",null,`score ${D9(Eu)}`),G("span",null,A?`selected ${A}`:"click node to control"))),G(DY,{epochs:Ku,activeRun:Eu,activePipeline:zu,pipelineNodes:P,pipelineEdges:e,selection:L,detailOpen:E,onDetailOpenChange:D,runDetails:N,nodeDetails:pu,nodeDetailsState:ff,ganttScale:W,onGanttScaleChange:z,onIntervalSelect:k0,onMarkerSelect:Ul,onRunChange:(fu)=>{if(c(fu),J(h0()),U(Rn()),D(!1),A)lr(fu,A)},onRaw:l}),G(F0,{title:"观测指标",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Snapshot",loading:n.loading},G("div",{className:"metric-grid"},G(pf,{label:"Health",value:n.health?.ok?"OK":"--",hint:n.health?.service||"D601 /health",tone:n.health?.ok?"ok":"warn"}),G(pf,{label:"组件",value:Uf,hint:"components registry",tone:b?.registry?.ok===!1?"warn":"ok"}),G(pf,{label:"Pipeline",value:lu.length,hint:`${P.length} nodes / ${e.length} edges`}),G(pf,{label:"运行记录",value:Ef,hint:`${mu.succeeded||0} succeeded / ${mu.running||0} running`}),G(pf,{label:"OA 记录",value:Array.isArray(uu?.submissions)?uu.submissions.length:0,hint:uu?.runId||"latest run"}),G(pf,{label:"Procedure",value:Array.isArray(uu?.procedureRuns)?uu.procedureRuns.length:0,hint:uu?.status||"no run"}),G(pf,{label:"Score",value:D9(Eu),hint:Eu?.runId||"selected epoch",tone:x9(Eu)})),G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline Snapshot",data:b,onOpen:l,testId:"raw-pipeline-snapshot"}))),G(F0,{title:"评分器",eyebrow:Eu?.runId||"selected epoch",loading:n.loading},G(vS,{run:Eu,onRaw:l})),G(F0,{title:"MiniMax 限额",eyebrow:"model/minimax-m27 quota",loading:n.loading},G(VY,{quota:g,onRaw:l})),G(F0,{title:"OA 事件流",eyebrow:"100% event-driven diagnostics",className:"pipeline-wide-panel",loading:n.loading},G(BY,{diagnostics:o,onRaw:l})),G(F0,{title:"组件矩阵",eyebrow:`${Fl.length} classes`,loading:n.loading},Fl.length===0?G(Qr,{title:"暂无组件",text:"等待 D601 pipeline backend 返回 registry.components"}):G("div",{className:"component-strata"},Fl.map((fu)=>G("article",{key:fu.name,className:"component-stratum"},G("span",null,fu.name),G("strong",null,fu.count)))),G("div",{className:"pipeline-component-list"},x.slice(0,12).map((fu)=>G("span",{key:fu.key,className:"data-chip"},G("b",null,fu.componentClass||"--"),G("span",null,fu.id||fu.key||"--"))))),G(F0,{title:"Epoch 列表",eyebrow:`${Ku.length}/${Ef} preview`,loading:n.loading},Ku.length===0?G(Qr,{title:"暂无运行记录",text:"当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。"}):G("div",{className:"pipeline-run-list"},Ku.map((fu)=>{let Bu=String(fu?.runId||"")===Hu?Eu:fu;return G("article",{key:fu.runId,className:`pipeline-run-card ${String(fu.runId||"")===Hu?"active":""}`,role:"button",tabIndex:0,onClick:()=>{c(String(fu.runId||"")),U(Rn())},onKeyDown:(Yu)=>{if(Yu.key==="Enter"||Yu.key===" ")c(String(fu.runId||"")),U(Rn())}},G("div",{className:"node-card-head"},G("strong",null,X9(Ku,fu)),G(bn,{status:fu.status},fu.status||"--")),G("div",{className:"docker-meta compact"},G("span",null,Bu?.pipelineId||"--"),G("span",null,`nodes ${Array.isArray(Bu?.nodes)?Bu.nodes.length:0}`),G("span",null,`oa ${Array.isArray(Bu?.submissions)?Bu.submissions.length:0}`),G("span",null,`procedures ${Array.isArray(Bu?.procedureRuns)?Bu.procedureRuns.length:0}`),G(bS,{run:Bu})),G("p",{className:"muted paragraph"},N6(Bu?.task)),G("span",{className:"pipeline-run-time"},qu(Bu?.updatedAt)))}))),G(F0,{title:"运行材料索引",eyebrow:Eu?.runId||"selected epoch",className:"pipeline-wide-panel",loading:n.loading},G(HY,{activeRun:Eu,onRaw:l}))))}var H6=Pu(Jl(),1);var tu=H6.default.createElement,{useEffect:SY}=H6.default,Z6=H6.default.useState,k9={id:"",sequenceNo:"",contractNo:"",name:"",currentStatus:"",pending:"",paymentStatus:"",notes:""};function YY({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return tu("span",{className:`status-badge ${f}`},l||u||"unknown")}function O6({label:u,value:l,hint:f,tone:r}){return tu("article",{className:`metric-card ${r||""}`},tu("div",{className:"metric-label"},u),tu("div",{className:"metric-value"},l),tu("div",{className:"metric-hint"},f))}function I9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return tu("section",{className:`panel ${n||""}`},tu("div",{className:"panel-head"},tu("div",null,l?tu("p",{className:"panel-eyebrow"},l):null,tu(nl,{title:u,loading:i})),f?tu("div",{className:"panel-actions"},f):null),tu("div",{className:"panel-body"},r))}function hw({title:u,data:l,onOpen:f,testId:r}){return tu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function bw({title:u,text:l}){return tu("div",{className:"empty-state"},tu("strong",null,u),tu("span",null,l))}function pY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function mY(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function PY(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function Ny(u,l){return`${u}/microservices/project-manager/proxy${l}`}function CY(u){return{id:String(u.id||""),sequenceNo:u.sequenceNo===null||u.sequenceNo===void 0?"":String(u.sequenceNo),contractNo:String(u.contractNo||""),name:String(u.name||""),currentStatus:String(u.currentStatus||""),pending:String(u.pending||""),paymentStatus:String(u.paymentStatus||""),notes:String(u.notes||"")}}function MY(u){return{sequenceNo:u.sequenceNo===""?null:Number(u.sequenceNo),contractNo:String(u.contractNo||"").trim(),name:String(u.name||"").trim(),currentStatus:String(u.currentStatus||"").trim(),pending:String(u.pending||"").trim(),paymentStatus:String(u.paymentStatus||"").trim(),paymentRatio:String(u.paymentStatus||"").trim(),notes:String(u.notes||"").trim()}}function g9(u){return String(u||"item").replace(/[^A-Za-z0-9_-]+/g,"-")}function RY(u){let l=new Uint8Array(u),f="",r=32768;for(let n=0;ntu("tr",{key:n.id,className:l===n.id?"active-row":"","data-testid":`project-manager-row-${g9(n.id)}`},tu("td",null,n.sequenceNo??"--"),tu("td",null,tu("strong",null,n.contractNo||"--"),tu("code",null,n.id||"--")),tu("td",null,tu("strong",null,n.name||"--"),tu("span",{className:"muted block"},n.sourceFile||"--")),tu("td",null,n.currentStatus||"--"),tu("td",null,tu("span",{className:"preline"},n.pending||"--")),tu("td",null,tu(YY,{status:Number(n.paymentRatio||0)>=1?"online":"warn"},n.paymentStatus||"--")),tu("td",null,n.notes||"--"),tu("td",null,tu("div",{className:"inline-actions"},tu("button",{type:"button",className:"ghost-btn",onClick:()=>f(n),"data-testid":`project-manager-edit-${g9(n.id)}`},"编辑"),tu(hw,{title:`Project ${n.contractNo||n.id}`,data:n,onOpen:r,testId:`raw-project-${g9(n.id)}`}))))))))}function vw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((E)=>E.id==="project-manager")||null,[n,i]=Z6({loading:!1,saving:!1,importing:!1,exporting:!1,error:"",notice:"",health:null,list:null,refreshedAt:null}),[y,t]=Z6({...k9}),[_,c]=Z6(""),[A,j]=Z6("all"),{addNotification:F}=Xf();async function J(E=_,D=A){if(!r)return;i((h)=>({...h,loading:!0,error:""}));try{let h=new URLSearchParams({pageSize:"200",status:D});if(E.trim())h.set("q",E.trim());let[V,S]=await Promise.all([Tu(`${f}/microservices/project-manager/health`),Tu(Ny(f,`/api/projects?${h.toString()}`))]);i((p)=>({...p,loading:!1,health:V,list:S,refreshedAt:new Date,error:""}))}catch(h){i((V)=>({...V,loading:!1,error:Ou(h,"Project Manager 加载失败")}))}}SY(()=>{J()},[r?.id,r?.runtime?.providerStatus]);async function Q(E){E.preventDefault(),i((D)=>({...D,saving:!0,error:"",notice:""}));try{let D=MY(y);if(y.id)await Tu(Ny(f,`/api/projects/${encodeURIComponent(y.id)}`),{method:"PUT",body:JSON.stringify(D)});else await Tu(Ny(f,"/api/projects"),{method:"POST",body:JSON.stringify(D)});let h=y.id?"项目已更新":"项目已创建";i((V)=>({...V,saving:!1,notice:h})),F("success",h),await J()}catch(D){i((h)=>({...h,saving:!1,error:Ou(D,"保存项目失败")}))}}async function w(){if(!y.id)return;if(!window.confirm(`删除项目 ${y.contractNo||y.name||y.id} ?`))return;i((E)=>({...E,saving:!0,error:"",notice:""}));try{await Tu(Ny(f,`/api/projects/${encodeURIComponent(y.id)}`),{method:"DELETE"}),t({...k9});let E="项目已删除";i((D)=>({...D,saving:!1,notice:E})),F("success",E),await J()}catch(E){i((D)=>({...D,saving:!1,error:Ou(E,"删除项目失败")}))}}async function L(E){let D=E.target.files?.[0];if(!D)return;i((h)=>({...h,importing:!0,error:"",notice:""}));try{let h=RY(await D.arrayBuffer()),S=`Excel 已导入 ${(await Tu(Ny(f,"/api/import/excel"),{method:"POST",body:JSON.stringify({fileName:D.name,contentBase64:h,replace:!1})})).imported||0} 条项目`;i((p)=>({...p,importing:!1,notice:S})),F("success",S),E.target.value="",await J()}catch(h){i((V)=>({...V,importing:!1,error:Ou(h,"Excel 导入失败")}))}}async function U(){i((E)=>({...E,exporting:!0,error:""}));try{let E=await cJ(Ny(f,"/api/projects/export.xlsx")),D=URL.createObjectURL(E),h=document.createElement("a");h.href=D,h.download=`project-manager-${x7()}.xlsx`,document.body.appendChild(h),h.click(),h.remove(),URL.revokeObjectURL(D),i((V)=>({...V,exporting:!1,notice:"Excel 已导出"}))}catch(E){i((D)=>({...D,exporting:!1,error:Ou(E,"Excel 导出失败")}))}}if(!r)return tu(bw,{title:"Project Manager 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=project-manager"});let N=pY(r),q=PY(r),W=mY(r),z=Array.isArray(n.list?.projects)?n.list.projects:[],Z=n.list?.summary||{},H=n.health||{};return tu("div",{className:"project-manager-page","data-testid":"project-manager-page"},tu(I9,{title:"项目管理工作台",eyebrow:"Main Server PostgreSQL 用户服务",loading:n.loading||n.exporting,actions:tu("div",{className:"panel-actions"},tu("button",{type:"button",className:"ghost-btn",disabled:n.loading,onClick:()=>J(),"data-testid":"project-manager-refresh-button"},n.loading?"刷新中":"刷新"),tu("button",{type:"button",className:"ghost-btn",disabled:n.exporting,onClick:U,"data-testid":"project-manager-export-button"},n.exporting?"导出中":"导出 Excel"),tu(hw,{title:"Project Manager 用户服务",data:r,onOpen:l,testId:"raw-project-manager-service"}))},tu("div",{className:"project-manager-hero"},tu(O6,{label:"项目总数",value:Z.total??z.length,hint:`PG 表 ${H.storage?.table||"project_manager_projects"}`,tone:"ok"}),tu(O6,{label:"进行中",value:Z.active??"--",hint:"当前状态未完全完成"}),tu(O6,{label:"已完成",value:Z.completed??"--",hint:"按 完成 关键字统计",tone:"ok"}),tu(O6,{label:"未全款",value:Z.unpaid??"--",hint:"付款比例 < 1",tone:Number(Z.unpaid||0)>0?"warn":"ok"})),tu(il,{error:n.error}),n.notice?tu("div",{className:"form-success"},n.notice):null),tu("div",{className:"project-manager-hero"},tu("div",{className:"microservice-ref-card"},tu("span",null,"Repo"),tu("strong",null,q.url||"--"),tu("code",null,q.commitId||"--")),tu("div",{className:"microservice-ref-card"},tu("span",null,"Main Server Docker"),tu("strong",null,`${W.nodeBindHost||"--"}:${W.nodePort||"--"}`),tu("code",null,`${q.composeService||"--"} / ${q.containerName||"--"}`)),tu("div",{className:"microservice-ref-card"},tu("span",null,"Runtime"),tu("strong",null,N.providerName||r.providerId),tu("code",null,`Health ${H.ok?"OK":"--"} / ${n.refreshedAt?tl(n.refreshedAt):"--"}`)),tu("div",{className:"microservice-ref-card"},tu("span",null,"Import Source"),tu("strong",null,"D601 WeChat Excel"),tu("code",null,"合作项目列表_I_20260309.xlsx"))),tu("div",{className:"project-manager-layout"},tu(I9,{title:"项目清单",eyebrow:"CRUD + Excel Export",loading:n.loading||n.importing||n.exporting,actions:tu("div",{className:"inline-actions project-manager-filters"},tu("input",{value:_,onChange:(E)=>c(E.target.value),placeholder:"搜索合同号 / 项目名称 / 状态","data-testid":"project-manager-search"}),tu("select",{value:A,onChange:(E)=>{j(E.target.value),J(_,E.target.value)},"data-testid":"project-manager-status-filter"},tu("option",{value:"all"},"全部"),tu("option",{value:"active"},"进行中"),tu("option",{value:"completed"},"已完成"),tu("option",{value:"unpaid"},"未全款")),tu("button",{type:"button",className:"ghost-btn",onClick:()=>J(_,A)},"筛选"))},tu(xY,{projects:z,activeId:y.id,onSelect:(E)=>t(CY(E)),onRaw:l})),tu(I9,{title:y.id?"编辑项目":"新建项目",eyebrow:"PostgreSQL Write Path",loading:n.saving||n.importing},tu("form",{className:"stack-form project-manager-form",onSubmit:Q,"data-testid":"project-manager-form"},y.id?tu("label",null,"项目 ID",tu("input",{value:y.id,disabled:!0})):null,tu("label",null,"序号",tu("input",{type:"number",value:y.sequenceNo,onChange:(E)=>t((D)=>({...D,sequenceNo:E.target.value}))})),tu("label",null,"合同号",tu("input",{value:y.contractNo,onChange:(E)=>t((D)=>({...D,contractNo:E.target.value})),required:!0})),tu("label",null,"项目名称",tu("input",{value:y.name,onChange:(E)=>t((D)=>({...D,name:E.target.value})),required:!0})),tu("label",null,"当前状况",tu("textarea",{value:y.currentStatus,onChange:(E)=>t((D)=>({...D,currentStatus:E.target.value}))})),tu("label",null,"待完成",tu("textarea",{value:y.pending,onChange:(E)=>t((D)=>({...D,pending:E.target.value}))})),tu("label",null,"付款情况",tu("input",{value:y.paymentStatus,onChange:(E)=>t((D)=>({...D,paymentStatus:E.target.value})),placeholder:"例如 1 / 0.5 / 50%"})),tu("label",null,"其它",tu("input",{value:y.notes,onChange:(E)=>t((D)=>({...D,notes:E.target.value}))})),tu("div",{className:"inline-actions"},tu("button",{type:"submit",className:"primary-btn",disabled:n.saving,"data-testid":"project-manager-save-button"},n.saving?"保存中":y.id?"保存修改":"创建项目"),tu("button",{type:"button",className:"ghost-btn",onClick:()=>t({...k9})},"清空"),y.id?tu("button",{type:"button",className:"danger-btn",disabled:n.saving,onClick:w,"data-testid":"project-manager-delete-button"},"删除"):null)),tu("div",{className:"project-manager-import"},tu("p",{className:"muted paragraph"},"浏览器只访问 UniDesk frontend;后端通过同源用户服务代理写入主 PostgreSQL,不暴露 4233 公网端口。"),tu("label",{className:"file-import"},n.importing?"导入中...":"导入 Excel",tu("input",{type:"file",accept:".xlsx",onChange:L,disabled:n.importing,"data-testid":"project-manager-import-input"}))))))}var D6=Pu(Jl(),1);var cu=D6.default.createElement,{useEffect:hY}=D6.default,df=D6.default.useState;function bY({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return cu("span",{className:`status-badge ${f}`},l||u||"unknown")}function B6({label:u,value:l,hint:f,tone:r}){return cu("article",{className:`metric-card ${r||""}`},cu("div",{className:"metric-label"},u),cu("div",{className:"metric-value"},l),cu("div",{className:"metric-hint"},f))}function s9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return cu("section",{className:`panel ${n||""}`},cu("div",{className:"panel-head"},cu("div",null,l?cu("p",{className:"panel-eyebrow"},l):null,cu(nl,{title:u,loading:i})),f?cu("div",{className:"panel-actions"},f):null),cu("div",{className:"panel-body"},r))}function kw({title:u,data:l,onOpen:f,testId:r}){return cu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function V6({title:u,text:l}){return cu("div",{className:"empty-state"},cu("strong",null,u),cu("span",null,l))}function vY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function kY(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function IY(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function gw(u){return String(u).replace(/[^a-zA-Z0-9_-]/g,"_")}function gY(u){if(!Number.isFinite(u))return"--";return`${u.toFixed(1)}%`}function qy(u,l){return`${u}/microservices/todo-note/proxy${l}`}function sw(u){return u.reduce((l,f)=>{let r=sw(Array.isArray(f.children)?f.children:[]),n=Boolean(f.completed);return{total:l.total+1+r.total,completed:l.completed+(n?1:0)+r.completed,active:l.active+(n?0:1)+r.active}},{total:0,completed:0,active:0})}function o9(u,l){let f=l==="all"||(l==="completed"?Boolean(u.completed):!u.completed),r=Array.isArray(u.children)?u.children:[];return f||r.some((n)=>o9(n,l))}function Iw(u){return Array.isArray(u?.instances)?u.instances:[]}function a9(u,l){for(let f of u){if(f?.id===l)return Array.isArray(f.children)?f.children:[];let r=a9(Array.isArray(f?.children)?f.children:[],l);if(r.length>0)return r}return[]}function aw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((s)=>s.id==="todo-note")||null,[n,i]=df(null),[y,t]=df(null),[_,c]=df(""),[A,j]=df(null),[F,J]=df("all"),[Q,w]=df(13),[L,U]=df(""),[N,q]=df(""),[W,z]=df(""),[Z,H]=df(""),[E,D]=df(""),[h,V]=df(!1),[S,p]=df(""),[O,m]=df(null),X=Iw(y),v=sw(Array.isArray(A?.todos)?A.todos:[]),T=r?vY(r):{},Y=r?IY(r):{},k=r?kY(r):{};async function I(s=_){let[Nu,Eu]=await Promise.all([Tu(`${f}/microservices/todo-note/health`),Tu(qy(f,"/api/instances"))]);i(Nu),t(Eu);let Hu=Iw(Eu),vu=Hu.some((ul)=>ul.id===s)?s:Hu[0]?.id||"";return c(vu),vu}async function b(s=_){if(!s){j(null);return}let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(s)}`));j(Nu)}async function o(s=_){if(!r)return;V(!0),p("");try{let Nu=await I(s);await b(Nu),m(new Date)}catch(Nu){p(Ou(Nu,"Todo Note 加载失败"))}finally{V(!1)}}async function g(s){if(!_)return null;p("");try{let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(_)}/actions`),{method:"POST",body:JSON.stringify({action:s})});return j(Nu),await I(_),Nu}catch(Nu){return p(Ou(Nu,"Todo 操作失败")),null}}async function x(s){s.preventDefault();let Nu=L.trim();if(!Nu)return;V(!0),p("");try{let Eu=await Tu(qy(f,"/api/instances"),{method:"POST",body:JSON.stringify({name:Nu})});U(""),await o(Eu.id)}catch(Eu){p(Ou(Eu,"创建清单失败"))}finally{V(!1)}}async function lu(s){if(!window.confirm("确认删除这个 Todo Note 清单?"))return;V(!0),p("");try{await Tu(qy(f,`/api/instances/${encodeURIComponent(s)}`),{method:"DELETE"}),await o(_===s?"":_)}catch(Nu){p(Ou(Nu,"删除清单失败"))}finally{V(!1)}}async function _u(s){s.preventDefault();let Nu=N.trim();if(!Nu)return;q(""),await g({type:"addTodo",title:Nu})}async function $u(s){if(!_)return;p("");try{let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(_)}/${s}`),{method:"POST",body:JSON.stringify({})});j(Nu),await I(_)}catch(Nu){p(Ou(Nu,`${s} 失败`))}}function ju(s){z(s.id),H(String(s.title||""))}async function zu(s){let Nu=Z.trim();if(z(""),H(""),Nu)await g({type:"updateTodoTitle",todoId:s,title:Nu})}async function Wu(s){let Eu=window.prompt("新增子任务标题")?.trim();if(!Eu)return;let Hu=a9(Array.isArray(A?.todos)?A.todos:[],s),vu=new Set(Hu.map((Uf)=>Uf.id)),ul=await g({type:"addTodo",title:Eu,parentId:s,targetIndex:0});if(!ul)return;let mu=a9(Array.isArray(ul?.todos)?ul.todos:[],s),Fl=mu.find((Uf)=>!vu.has(Uf.id));if(Fl&&mu[0]?.id!==Fl.id)await g({type:"moveTodo",todoId:Fl.id,targetParentId:s,targetIndex:0})}async function P(s,Nu){if(!E)return;let Eu={type:"moveTodo",todoId:E,targetIndex:Nu};if(s)Eu.targetParentId=s;D(""),await g(Eu)}if(hY(()=>{o()},[r?.id,r?.runtime?.providerStatus]),!r)return cu(V6,{title:"Todo Note 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=todo-note"});let e=X.find((s)=>s.id===_)||null,uu=Array.isArray(A?.todos)?A.todos:[],Ku=uu.map((s,Nu)=>({todo:s,index:Nu})).filter((s)=>o9(s.todo,F));return cu("div",{className:"todo-note-page","data-testid":"todo-note-page"},cu(s9,{title:"Todo Note 工作台",eyebrow:"Main Server 用户服务",loading:h,actions:cu("div",{className:"panel-actions"},cu("button",{type:"button",className:"ghost-btn",disabled:h,onClick:()=>o(_),"data-testid":"todo-note-refresh-button"},h?"刷新中":"刷新"),cu(kw,{title:"Todo Note 用户服务",data:r,onOpen:l,testId:"raw-todo-note-service"}))},cu("div",{className:"todo-note-hero"},cu("div",null,cu("div",{className:"node-version-line"},cu(bY,{status:T.providerStatus==="online"?"online":"warn"},T.providerStatus||"unknown"),cu("span",null,r.providerId),cu("span",null,k.public?"公网暴露":"仅 UniDesk frontend 代理访问"),cu("span",null,n?.ok?"Health OK":"Health --")),cu("p",{className:"muted paragraph"},r.description)),cu("div",{className:"microservice-ref-card"},cu("span",null,"Repo"),cu("strong",null,Y.url||"--"),cu("code",null,Y.commitId||"--")),cu("div",{className:"microservice-ref-card"},cu("span",null,"Main Server Docker"),cu("strong",null,`${k.nodeBindHost||"--"}:${k.nodePort||"--"}`),cu("code",null,`${Y.composeService||"--"} / ${Y.containerName||"--"}`))),cu(il,{error:S,wide:!0})),cu("div",{className:"todo-note-layout"},cu(s9,{title:"清单",eyebrow:`${X.length} Instances`,className:"todo-list-panel",loading:h},cu("form",{className:"todo-create-list",onSubmit:x},cu("input",{placeholder:"新清单名称",value:L,onChange:(s)=>U(s.target.value),"aria-label":"新清单名称"}),cu("button",{type:"submit",className:"ghost-btn",disabled:h||!L.trim()},"创建")),X.length===0?cu(V6,{title:"暂无清单",text:"迁移或创建清单后会出现在这里"}):cu("div",{className:"todo-instance-list"},X.map((s)=>cu("button",{key:s.id,type:"button",className:`todo-instance-row ${_===s.id?"active":""}`,onClick:()=>{c(s.id),b(s.id)},"data-testid":`todo-instance-${gw(s.id)}`},cu("strong",null,s.name),cu("span",null,`${s.completedCount??0}/${s.todoCount??0} 完成`),cu("code",null,s.id))))),cu("div",{className:"todo-main-stack"},cu(s9,{title:e?.name||"待选择清单",eyebrow:O?`Updated ${tl(O)}`:"Todo Tree",loading:h,actions:A?cu("div",{className:"panel-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"renameInstance",name:window.prompt("清单新名称",A.name)||A.name})},"重命名"),cu("button",{type:"button",className:"ghost-btn danger",onClick:()=>lu(_)},"删除清单"),cu(kw,{title:`Todo Instance ${_}`,data:A,onOpen:l,testId:"raw-todo-instance"})):null},!A?cu(V6,{title:"未选择清单",text:"左侧选择一个 Todo Note 清单"}):cu("div",{className:"todo-workbench",style:{"--todo-font-size":`${Q}px`}},cu("div",{className:"todo-toolbar"},cu("form",{className:"todo-add-form",onSubmit:_u},cu("input",{placeholder:"新增根任务",value:N,onChange:(s)=>q(s.target.value),"aria-label":"新增根任务"}),cu("button",{type:"submit",className:"ghost-btn",disabled:!N.trim()},"新增")),cu("div",{className:"todo-filter-strip"},["all","active","completed"].map((s)=>cu("button",{key:s,type:"button",className:`todo-filter ${F===s?"active":""}`,onClick:()=>J(s)},s==="all"?"全部":s==="active"?"未完成":"已完成"))),cu("div",{className:"todo-toolbar-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"setAllTodosExpanded",expanded:!0})},"全部展开"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"setAllTodosExpanded",expanded:!1})},"全部收起"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>$u("undo")},"撤销"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>$u("redo")},"重做"),cu("label",{className:"todo-font-control"},"字号",cu("input",{type:"range",min:11,max:18,value:Q,onChange:(s)=>w(Number(s.target.value))})))),cu("div",{className:"todo-stats-grid"},cu(B6,{label:"总任务",value:v.total,hint:`${X.length} lists`}),cu(B6,{label:"已完成",value:v.completed,hint:`${gY(v.total?v.completed/v.total*100:0)}`,tone:"ok"}),cu(B6,{label:"未完成",value:v.active,hint:F==="active"?"当前筛选":"active tasks",tone:v.active>0?"warn":"ok"}),cu(B6,{label:"历史指针",value:A.historyPointer??0,hint:"undo / redo"})),cu("div",{className:"todo-root-drop",onDragOver:(s)=>s.preventDefault(),onDrop:(s)=>{s.preventDefault(),P(null,uu.length)}},"拖到这里可移为根任务末尾"),cu("div",{className:"todo-tree","data-testid":"todo-note-tree"},Ku.length===0?cu(V6,{title:"没有匹配任务",text:"调整筛选或新增任务"}):Ku.map(({todo:s,index:Nu})=>cu(ow,{key:s.id,todo:s,depth:0,parentId:null,index:Nu,siblingCount:uu.length,filter:F,editingId:W,editingTitle:Z,setEditingTitle:H,beginEdit:ju,saveEdit:zu,applyTodoAction:g,addChild:Wu,dragTodoId:E,setDragTodoId:D,dropTodo:P}))))))))}function ow(u){let{todo:l,depth:f,parentId:r,index:n,siblingCount:i,filter:y,editingId:t,editingTitle:_,setEditingTitle:c,beginEdit:A,saveEdit:j,applyTodoAction:F,addChild:J,dragTodoId:Q,setDragTodoId:w,dropTodo:L}=u,U=Array.isArray(l.children)?l.children:[],N=U.map((z,Z)=>({child:z,childIndex:Z})).filter((z)=>o9(z.child,y)),q=t===l.id,W=r||null;return cu("div",{className:"todo-row-wrap"},cu("article",{className:`todo-row ${l.completed?"completed":""} ${Q===l.id?"dragging":""}`,style:{"--todo-depth":f},draggable:!0,onDragStart:(z)=>{w(l.id),z.dataTransfer.effectAllowed="move"},onDragOver:(z)=>z.preventDefault(),onDrop:(z)=>{z.preventDefault(),L(l.id,U.length)},"data-testid":`todo-row-${gw(l.id)}`},cu("button",{type:"button",className:"todo-expand",disabled:U.length===0,onClick:()=>F({type:"toggleTodoExpanded",todoId:l.id})},U.length===0?"·":l.expanded?"▾":"▸"),cu("input",{type:"checkbox",checked:Boolean(l.completed),onChange:()=>F({type:"toggleTodoCompleted",todoId:l.id}),"aria-label":`完成 ${l.title}`}),cu("div",{className:"todo-title-cell",onDoubleClick:()=>A(l)},q?cu("div",{className:"todo-edit-inline"},cu("input",{value:_,autoFocus:!0,onChange:(z)=>c(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")j(l.id);if(z.key==="Escape")A({id:"",title:""})}}),cu("button",{type:"button",className:"ghost-btn",onClick:()=>j(l.id)},"保存")):cu("strong",null,l.title||"Untitled"),cu("div",{className:"todo-meta-line"},cu("span",null,`子项 ${U.length}`),cu("span",null,`更新 ${qu(l.updatedAt)}`),l.reminderAt?cu("span",{className:"todo-reminder"},`提醒 ${qu(l.reminderAt)}`):cu("span",null,"无提醒"))),cu("input",{className:"todo-reminder-input",type:"datetime-local",value:r8(l.reminderAt),onChange:(z)=>F({type:"setTodoReminder",todoId:l.id,reminderAt:h7(z.target.value)})}),cu("div",{className:"todo-row-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>A(l)},"编辑"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>J(l.id)},"子项"),cu("button",{type:"button",className:"ghost-btn",disabled:n<=0,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:n-1})},"上移"),cu("button",{type:"button",className:"ghost-btn",disabled:n<=0,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:0})},"置顶"),cu("button",{type:"button",className:"ghost-btn",disabled:n>=i-1,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:n+1})},"下移"),cu("button",{type:"button",className:"ghost-btn",disabled:!r,onClick:()=>F({type:"moveTodo",todoId:l.id,targetIndex:9999})},"提升"),cu("button",{type:"button",className:"ghost-btn danger",onClick:()=>F({type:"deleteTodo",todoId:l.id})},"删除"))),l.expanded&&N.length>0?cu("div",{className:"todo-children"},N.map(({child:z,childIndex:Z})=>cu(ow,{key:z.id,todo:z,depth:f+1,parentId:l.id,index:Z,siblingCount:U.length,filter:y,editingId:t,editingTitle:_,setEditingTitle:c,beginEdit:A,saveEdit:j,applyTodoAction:F,addChild:J,dragTodoId:Q,setDragTodoId:w,dropTodo:L}))):null)}var dw=Pu(Jl(),1),kn=dw.default.createElement;function ew({title:u,items:l,actions:f,className:r,testId:n}){let i=Array.isArray(l)?l:[];return kn("section",{className:`top-status-bar ${r||""}`,"data-testid":n},kn("div",{className:"top-status-main"},u?kn("strong",{className:"top-status-title"},u):null,kn("div",{className:"top-status-chips"},i.map((y,t)=>kn("span",{key:y?.key||`${y?.label||"status"}-${t}`,className:`top-status-chip ${y?.tone||""}`,"data-testid":y?.testId},y?.label?kn("b",null,y.label):null,kn("span",null,y?.value??"--"))))),f?kn("div",{className:"top-status-actions"},f):null)}var M_=Pu(Jl(),1);var Qu=M_.default.createElement,{useEffect:sY,useMemo:aY}=M_.default,oY=M_.default.useState;function uL({status:u,children:l,title:f}){let r=String(u||"unknown").toLowerCase();return Qu("span",{className:`status-badge ${r}`,title:f},l||u||"unknown")}function X6({label:u,value:l,hint:f,tone:r}){return Qu("article",{className:`metric-card ${r||""}`},Qu("div",{className:"metric-label"},u),Qu("div",{className:"metric-value"},l),Qu("div",{className:"metric-hint"},f))}function d9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return Qu("section",{className:`panel ${n||""}`},Qu("div",{className:"panel-head"},Qu("div",null,l?Qu("p",{className:"panel-eyebrow"},l):null,Qu(nl,{title:u,loading:i})),f?Qu("div",{className:"panel-actions"},f):null),Qu("div",{className:"panel-body"},r))}function lL({title:u,data:l,onOpen:f,testId:r}){return Qu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f?.(u,l)},"查看原始JSON")}function e9({title:u,text:l}){return Qu("div",{className:"empty-state"},Qu("strong",null,u),Qu("span",null,l))}function Wy(u){return Array.isArray(u)?u:[]}function u7(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function dY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function eY(u,l){return`${u}/microservices/v3sctl-adapter/proxy${l}`}function up(u){return u.find((l)=>String(l?.id||"")==="v3sctl-adapter")||null}function lp(u){if(u?.healthy===!0)return"online";if(String(u?.role||"")==="standby")return"warn";return"failed"}function fp(u){return u?.healthy===!0?"online":"failed"}function rp(u){if(u===!0)return"YES";if(u===!1)return"NO";return"--"}function np(u){return Array.from(new Set(u.flatMap((l)=>Wy(l?.expectedNodeIds).map((f)=>String(f))))).filter(Boolean).sort()}function ip(u){let l=u.find((f)=>f?.id==="code-queue")||u[0];return String(l?.activeInstanceId||"--")}function yp(u){return Qu("article",{key:u?.id||u?.nodeId,className:"v3s-instance-card"},Qu("div",{className:"node-card-head"},Qu("strong",null,u?.nodeId||u?.id||"--"),Qu(uL,{status:lp(u)},u?.healthy?"HEALTHY":"DEGRADED")),Qu("div",{className:"v3s-instance-role"},Qu("span",null,String(u?.role||"worker").toUpperCase()),Qu("code",null,u?.id||"--")),Qu("dl",{className:"v3s-kv"},Qu("dt",null,"Base URL"),Qu("dd",null,Qu("code",null,u?.baseUrl||"--")),Qu("dt",null,"Proxy"),Qu("dd",null,u?.proxyMode||"--"),Qu("dt",null,"Health"),Qu("dd",null,`${u?.upstreamStatus??"--"} / ${u?.status||"unknown"}`),Qu("dt",null,"Checked"),Qu("dd",null,qu(u?.checkedAt))))}function tp(u,l){let f=Wy(u?.instances),r=u7(u?.active);return Qu(d9,{key:u?.id||"service",title:u?.id||"managed-service",eyebrow:`${u?.namespace||"unidesk"} / v3s managed service`,className:"v3s-service-panel",actions:Qu(lL,{title:`v3s service ${u?.id||""}`,data:u,onOpen:l,testId:`raw-v3s-service-${u?.id||"unknown"}`})},Qu("div",{className:"v3s-service-summary"},Qu("div",null,Qu("span",null,"状态"),Qu(uL,{status:fp(u)},u?.status||"unknown")),Qu("div",null,Qu("span",null,"Active"),Qu("strong",null,u?.activeInstanceId||"--")),Qu("div",null,Qu("span",null,"Single Writer"),Qu("strong",null,rp(u?.singleWriter))),Qu("div",null,Qu("span",null,"Active Health"),Qu("strong",null,r?.upstreamStatus??"--"))),f.length===0?Qu(e9,{title:"暂无 v3s 实例",text:"adapter 没有返回该服务的 endpoint 列表"}):Qu("div",{className:"v3s-instance-grid"},f.map(yp)))}function fL({microservices:u,onRaw:l,apiBaseUrl:f,onNavigate:r}){let n=up(Array.isArray(u)?u:[]),i=dY(n),[y,t]=oY({loading:!1,error:"",data:null,refreshedAt:null});async function _(){t((N)=>({...N,loading:!0,error:""}));try{let N=await Tu(eY(f,"/api/control-plane"));t({loading:!1,error:"",data:N,refreshedAt:new Date})}catch(N){t((q)=>({...q,loading:!1,error:Ou(N,"加载 v3s 控制平面失败")}))}}sY(()=>{_()},[f]);let c=aY(()=>Wy(y.data?.services),[y.data]),A=np(c),j=c.filter((N)=>N?.healthy===!0).length,F=c.reduce((N,q)=>N+Wy(q?.instances).length,0),J=c.reduce((N,q)=>N+Wy(q?.instances).filter((W)=>W?.healthy===!0).length,0),Q=ip(c),w=u7(y.data?.kubectl),L=u7(y.data?.kubeApiProxy),U=Wy(y.data?.manifestPaths).map((N)=>String(N));if(!n)return Qu(e9,{title:"v3sctl-adapter 未登记",text:"请在 config.json 的 microservices 中登记 id=v3sctl-adapter,并通过该微服务连接 v3s 控制平面。"});return Qu("div",{className:"v3s-page","data-testid":"v3sctl-page"},Qu(d9,{title:"V3S Control Plane",eyebrow:"Managed by v3sctl-adapter",className:"v3s-hero-panel",loading:y.loading,actions:Qu(M_.default.Fragment,null,Qu("button",{type:"button",className:"ghost-btn",onClick:_,disabled:y.loading,"data-testid":"v3s-refresh-button"},y.loading?"刷新中":"刷新"),r?Qu("button",{type:"button",className:"ghost-btn",onClick:()=>r("apps","code-queue"),"data-testid":"v3s-open-code-queue"},"打开 Code Queue"):null,Qu(lL,{title:"v3sctl-adapter microservice",data:n,onOpen:l,testId:"raw-v3s-adapter"}))},Qu("div",{className:"v3s-hero"},Qu("div",{className:"v3s-orb","aria-hidden":"true"},Qu("span",null,"v3s")),Qu("div",{className:"v3s-hero-copy"},Qu("p",{className:"eyebrow"},"D601 control plane / D518 managed node"),Qu("h2",null,"UniDesk 只管理 adapter;业务微服务交给 v3s 标准服务路由"),Qu("p",{className:"muted paragraph"},"Code Queue 的前端/API 请求进入 v3sctl-adapter,再由 adapter 转发到 v3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"),Qu("div",{className:"v3s-route-strip"},Qu("span",null,"NO FALLBACK"),Qu("code",null,y.data?.runtimePath||"frontend -> backend-core -> v3sctl-adapter")))),Qu("div",{className:"metric-grid"},Qu(X6,{label:"控制面",value:y.data?.clusterId||"D601",hint:`adapter ${i.providerStatus||"unknown"}`,tone:i.providerStatus==="online"?"ok":"warn"}),Qu(X6,{label:"代管服务",value:c.length,hint:`${j}/${c.length||0} healthy`,tone:j===c.length&&c.length>0?"ok":"warn"}),Qu(X6,{label:"节点",value:A.join(" / ")||"--",hint:"expected v3s nodes"}),Qu(X6,{label:"实例",value:`${J}/${F}`,hint:`active ${Q}`,tone:J===F&&F>0?"ok":"warn"})),Qu("div",{className:"v3s-control-plane-grid"},Qu("article",{className:"v3s-control-plane-card"},Qu("span",null,"service proxy"),Qu("strong",null,L.configured===!0?"K8S API PROXY":"PROXY DEGRADED"),Qu("p",null,L.configured===!0?`${L.mode||"kubernetes-api-service-proxy"} via ${L.connectHost||"--"}`:"adapter 必须通过 k8s API service proxy 访问业务服务,不回退到业务容器直连。")),Qu("article",{className:"v3s-control-plane-card"},Qu("span",null,"manifests"),Qu("strong",null,U.length||"--"),Qu("p",null,U.join(" / ")||"未配置 manifest")),Qu("article",{className:"v3s-control-plane-card"},Qu("span",null,"cluster snapshot"),Qu("strong",null,w.enabled===!0?w.ok===!0?"KUBECTL OK":"KUBECTL DEGRADED":"API ONLY"),Qu("p",null,w.enabled===!0?`nodes ${w.nodeCount??"--"}`:"控制面页面以 adapter 返回的 k8s service proxy 状态为准;kubectl 只作为可选快照。"))),y.error?Qu(il,{error:y.error}):null,y.refreshedAt?Qu("p",{className:"muted paragraph"},`最近刷新 ${tl(y.refreshedAt)}`):null),c.length===0?Qu(d9,{title:"代管服务",eyebrow:"v3s services",loading:y.loading},Qu(e9,{title:"暂无 v3s 服务",text:"等待 v3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。"})):c.map((N)=>tp(N,l)))}var R_=Pu(Jl(),1);var hl=R_.default.createElement;function rL({onClose:u}){let{notifications:l,removeNotification:f,clearNotifications:r}=Xf(),n=R_.default.useRef(null);if(R_.default.useEffect(()=>{let i=(y)=>{if(n.current&&!n.current.contains(y.target))u()};return document.addEventListener("mousedown",i),()=>document.removeEventListener("mousedown",i)},[u]),l.length===0)return hl("div",{className:"notification-popup",ref:n},hl("div",{className:"notification-popup-header"},hl("span",null,"通知"),hl("button",{className:"notification-popup-close",onClick:u},"×")),hl("div",{className:"notification-popup-empty"},"暂无通知"));return hl("div",{className:"notification-popup",ref:n},hl("div",{className:"notification-popup-header"},hl("span",null,`通知 (${l.length})`),hl("div",{className:"notification-popup-actions"},hl("button",{className:"notification-popup-clear",onClick:r},"清空"),hl("button",{className:"notification-popup-close",onClick:u},"×"))),hl("div",{className:"notification-popup-list"},l.slice().reverse().map((i)=>hl("div",{key:i.id,className:`notification-item ${i.type}`},hl("span",{className:"notification-item-icon"},i.type==="success"?"✓":"×"),hl("span",{className:"notification-item-message"},i.message),hl("button",{className:"notification-item-dismiss",onClick:()=>f(i.id)},"×")))))}function nL({notification:u}){let{removeNotification:l}=Xf();return R_.default.useEffect(()=>{let f=setTimeout(()=>{l(u.id)},3000);return()=>clearTimeout(f)},[u.id,l]),hl("div",{className:`notification-banner ${u.type}`,role:"alert"},hl("span",{className:"notification-banner-icon"},u.type==="success"?"✓":"×"),hl("span",{className:"notification-banner-message"},u.message),hl("button",{className:"notification-banner-dismiss",onClick:()=>l(u.id)},"×"))}function QL(u,l){let f=document.getElementById("root")?.getAttribute(u);if(!f)return l;try{let r=JSON.parse(f);return typeof r==="object"&&r!==null&&!Array.isArray(r)?r:l}catch{return l}}var gu=QL("data-config",{apiBaseUrl:"/api",authUsername:"admin"}),_p=QL("data-codex-overview",null),$=gn.default.createElement,{useEffect:J0,useMemo:b_}=gn.default,bu=gn.default.useState,n7=gn.default.createContext(!1),Dr=oQ(ic),$p={id:"code-queue",name:"Code Queue",providerId:"D601",description:"Code Queue",repository:{containerName:"v3s:code-queue"},backend:{nodeBaseUrl:"v3s://code-queue",nodeBindHost:"v3s://unidesk/code-queue",nodePort:4222,proxyMode:"v3sctl-adapter-http",public:!1},deployment:{mode:"v3sctl-managed",adapterServiceId:"v3sctl-adapter",v3sServiceId:"code-queue"},runtime:{orchestrator:"v3sctl",providerStatus:"loading",providerName:"D601"}};function iL(){return typeof document>"u"||document.visibilityState!=="hidden"}function cp(u,l){if(u==="ops"&&l==="status")return 5000;if(u==="nodes"&&l==="monitor")return 5000;if(u==="tasks"&&(l==="dispatch"||l==="scheduled"||l==="pending"))return 5000;if(u==="nodes"||u==="ops")return 1e4;if(u==="apps")return 15000;if(u==="tasks")return 15000;return 30000}async function Ap(u){if(!u?._summaryOnly||!u?.id)return u;return(await Tu(`${gu.apiBaseUrl}/tasks/${encodeURIComponent(String(u.id))}`))?.task||u}function v_(u){return u?._summaryOnly?{...u,_loadRaw:()=>Ap(u)}:u}function b0(u){if(!Number.isFinite(u))return"--";let l=Math.max(0,u);if(l===0)return"0s";if(l<0.01)return"<0.01s";if(l<0.1)return`${l.toFixed(2)}s`;if(l<1)return`${l.toFixed(1)}s`;if(l<10&&!Number.isInteger(l))return`${l.toFixed(1)}s`;if(l<60)return`${Math.round(l)}s`;let f=Math.floor(l);if(f<3600)return`${Math.floor(f/60)}m ${f%60}s`;return`${Math.floor(f/3600)}h ${Math.floor(f%3600/60)}m`}function Nr(u){let l=Number(u);if(!Number.isFinite(l))return"--";if(l<1)return`${Math.max(0,l).toFixed(1)}ms`;if(l<10)return`${l.toFixed(1)}ms`;if(l<1000)return`${Math.round(l)}ms`;return b0(l/1000)}function Tf(u){let l=Number(u);if(!Number.isFinite(l)||l<=0)return"--";let f=["B","KB","MB","GB","TB"],r=l,n=0;while(r>=1024&&n0)return f[r]}return"任务失败但 provider 未返回明确原因"}function hi(u){if(u===null||u===void 0)return"--";if(typeof u==="boolean")return u?"是":"否";if(typeof u==="number")return String(u);if(typeof u==="string")return u.length>80?`${u.slice(0,77)}...`:u;if(Array.isArray(u))return`${u.length} 项`;if(typeof u==="object")return`${Object.keys(u).length} 字段`;return String(u)}function Fp(u,l){let f=u.replace(/[-_\s]/g,"").toLowerCase(),r=f==="ts"||f.endsWith("at")||f.endsWith("timestamp")||f.endsWith("heartbeat");if((typeof l==="string"||typeof l==="number")&&r){let n=qu(l);if(n!=="--")return n}if(u==="bodyText"&&typeof l==="string")return`${/^\s*[{[]/.test(l)?"JSON":"HTTP"} body ${l.length} chars`;return hi(l)}function WL(u){if(!u||typeof u!=="object"||Array.isArray(u))return[];return Object.entries(u)}function ef(u){return String(u).replace(/[^a-zA-Z0-9_-]/g,"_")}function y7(u,l){return u&&typeof u==="object"&&!Array.isArray(u)?u[l]:void 0}function Y6(u,l,f="未知"){let r=y7(u?.labels,l);return typeof r==="string"&&r.length>0?r:f}function wL(u){return Y6(u,"providerGatewayVersion")}function h_(u){return Y6(u,"providerGatewayUpgradePolicy")}function yL(u){return Y6(u,"providerGatewayStartedAt","")}function LL(u){let l=y7(u?.labels,"unideskCapabilities");if(typeof l==="string")return l.split(",").map((f)=>f.trim()).filter(Boolean);return Array.isArray(l)?l.filter((f)=>typeof f==="string"):[]}function KL(u,l){return LL(u).includes(l)}function tL(u,l){let f=y7(u?.labels,l);return f===!0||f==="true"||f==="1"}function Up(u){if(!KL(u,"host.ssh"))return{tone:"fail",label:"不可用",detail:"未声明 host.ssh"};if(!tL(u,"hostSshConfigured"))return{tone:"warn",label:"未配置",detail:"缺少 SSH 环境变量"};if(!tL(u,"hostSshKeyPresent"))return{tone:"warn",label:"缺 key",detail:"私钥未挂载"};return{tone:"ok",label:"可用",detail:Y6(u,"hostSshTarget","host.ssh ready")}}function Jp(u){if(!KL(u,"provider.upgrade"))return{tone:"fail",label:"不可用",detail:"未声明 provider.upgrade"};let l=h_(u);if(l!=="always-enabled")return{tone:"warn",label:"待确认",detail:`策略 ${l}`};return{tone:"ok",label:"可用",detail:"always-enabled"}}function t7(u){let l=typeof u==="string"&&u.length>0?u:"未知";if(l==="未知")return"版本未知";return l.startsWith("v")?l:`v${l}`}function GL(u){return u?.payload&&typeof u.payload==="object"&&!Array.isArray(u.payload)?u.payload:{}}function p6(u){return u?.result&&typeof u.result==="object"&&!Array.isArray(u.result)?u.result:{}}function S6(u){let l=GL(u),f=p6(u);return(l.mode??f.mode)==="schedule"?"schedule":"plan"}function Qp(u){let l=GL(u).source;return typeof l==="string"&&l.length>0?l:"unknown"}function Np(u){let l=p6(u),f=l.plan&&typeof l.plan==="object"&&!Array.isArray(l.plan)?l.plan:{},r=l.policy??f.policy;return typeof r==="string"&&r.length>0?r:"--"}function zL(u){let l=p6(u),f=l.plan&&typeof l.plan==="object"&&!Array.isArray(l.plan)?l.plan:{},r=l.targetProviderGatewayVersion??l.providerGatewayVersion??f.targetProviderGatewayVersion??f.providerGatewayVersion;return typeof r==="string"&&r.length>0?t7(r):"版本未知"}function TL(u){if(String(u?.status||"").toLowerCase()==="failed")return qL(u);if(Ly(u))return"等待 provider 回传升级终态";let f=p6(u);if(typeof f.updaterContainerId==="string"&&f.updaterContainerId.length>0)return`updater ${f.updaterContainerId.slice(0,18)}`;if(typeof f.message==="string"&&f.message.length>0)return f.message;if(f.plan)return"升级计划已生成";return"无升级结果摘要"}function EL(u,l){return u.filter((f)=>f?.providerId===l&&f?.command==="provider.upgrade").sort((f,r)=>(Q0(r.updatedAt)??0)-(Q0(f.updatedAt)??0))}function qp(u){return u.find((l)=>S6(l)==="schedule")||u[0]||null}function ZL(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function _L(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function Wp(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function wl({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return $("span",{className:`status-badge ${f}`},l||u||"unknown")}function cl({label:u,value:l,hint:f,tone:r,onClick:n,testId:i}){let y=typeof n==="function";return $("article",{className:`metric-card ${r||""} ${y?"clickable":""}`,role:y?"button":void 0,tabIndex:y?0:void 0,"data-testid":i,onClick:n,onKeyDown:y?(t)=>{if(t.key==="Enter"||t.key===" ")t.preventDefault(),n()}:void 0},$("div",{className:"metric-label"},u),$("div",{className:"metric-value"},l),$("div",{className:"metric-hint"},f))}function du({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){let y=gn.default.useContext(n7),t=Boolean(i)||y;return $("section",{className:`panel ${n||""}`},$("div",{className:"panel-head"},$("div",null,l?$("p",{className:"panel-eyebrow"},l):null,$(nl,{title:u,loading:t})),f?$("div",{className:"panel-actions"},f):null),$("div",{className:"panel-body"},r))}function bl({title:u,data:l,onOpen:f,testId:r}){let[n,i]=bu(!1),y=l&&typeof l==="object"&&typeof l._loadRaw==="function"?l._loadRaw:null;async function t(){if(!y){f(u,l);return}i(!0);try{f(u,await y())}catch(_){f(u,{ok:!1,error:Ou(_,"读取原始 JSON 失败"),fallback:l})}finally{i(!1)}}return $("button",{type:"button",className:"ghost-btn","data-testid":r,disabled:n,onClick:()=>void t()},n?"读取中":"查看原始JSON")}function wp({raw:u,onClose:l}){if(!u)return null;return $("div",{className:"modal-backdrop",role:"presentation"},$("section",{className:"raw-dialog",role:"dialog","aria-modal":"true","aria-label":u.title},$("div",{className:"raw-dialog-head"},$("h2",null,u.title),$("button",{type:"button",className:"ghost-btn",onClick:l},"关闭")),$("pre",{className:"raw-json","data-testid":"raw-json"},JSON.stringify(u.data,null,2))))}function OL({labels:u,limit:l=8}){let f=WL(u).slice(0,l);if(f.length===0)return $("span",{className:"muted"},"无标签");return $("div",{className:"chip-row"},f.map(([r,n])=>$("span",{key:r,className:"data-chip"},$("b",null,r),$("span",null,hi(n)))))}function wy({node:u}){let l=wL(u);return $("span",{className:`version-chip ${l==="未知"?"unknown":""}`,"data-testid":`gateway-version-${ef(u?.providerId||"unknown")}`},t7(l))}function $L({title:u,state:l,testId:f}){return $("span",{className:`capability-badge ${l.tone}`,title:l.detail,"data-testid":f},$("b",null,u),$("strong",null,l.label),$("small",null,l.detail))}function _7({node:u}){let l=ef(u?.providerId||"unknown");return $("div",{className:"node-availability-strip"},$($L,{title:"SSH 透传",state:Up(u),testId:`ssh-availability-${l}`}),$($L,{title:"远程更新",state:Jp(u),testId:`upgrade-availability-${l}`}))}function sn({data:u,empty:l="无数据"}){if(u===null||u===void 0)return $("span",{className:"muted"},l);if(typeof u!=="object")return $("span",{className:"summary-value"},hi(u));if(Array.isArray(u))return $("span",{className:"summary-value"},`${u.length} 项列表`);let f=Object.entries(u).slice(0,5);if(f.length===0)return $("span",{className:"muted"},l);return $("div",{className:"summary-grid"},f.map(([r,n])=>$("span",{key:r,className:"summary-item"},$("b",null,r),$("span",null,Fp(r,n)))))}function jl({title:u,text:l}){return $("div",{className:"empty-state"},$("strong",null,u),$("span",null,l))}function Lp({onLogin:u}){let[l,f]=bu(gu.authUsername||"admin"),[r,n]=bu(""),[i,y]=bu(""),[t,_]=bu(!1);async function c(A){A.preventDefault(),_(!0),y("");try{let j=await Tu("/login",{method:"POST",body:JSON.stringify({username:l,password:r})});u(j)}catch(j){y(Ou(j,"登录失败"))}finally{_(!1)}}return $("main",{className:"login-screen","data-testid":"login-screen"},$("section",{className:"login-card"},$("div",{className:"login-brand"},$("span",{className:"brand-mark"},"UD"),$("div",null,$("h1",null,"UniDesk"),$("p",null,"Control Plane Login"))),$("form",{className:"login-form",onSubmit:c},$("label",null,"账号",$("input",{name:"username",autoComplete:"username",value:l,onChange:(A)=>f(A.target.value)})),$("label",null,"密码",$("input",{name:"password",type:"password",autoComplete:"current-password",value:r,onChange:(A)=>n(A.target.value)})),$(il,{error:i}),$("button",{type:"submit",disabled:t},t?"登录中":"登录")),$("div",{className:"login-note"},"默认账号由 config.json 注入;公网入口只暴露前端登录面。")))}function Kp({connection:u,lastRefresh:l,onRefresh:f,onLogout:r,session:n,clock:i,activeStatusItems:y=[],onNotificationToggle:t,unreadCount:_=0}){let c=[{key:"core",label:"核心",value:u.text,tone:u.ok?"ok":"fail",testId:"conn-text"},...Array.isArray(y)?y:[],{key:"refresh",label:"刷新",value:l?tl(l):"未刷新"},{key:"clock",label:c$,value:tl(i)},{key:"user",label:"用户",value:n?.user?.username||"--",tone:"user"}];return $("header",{className:"topbar"},$("div",null,$("p",{className:"eyebrow"},"Distributed Work Platform"),$("h1",null,"UniDesk 控制平面")),$(ew,{className:"global-top-status",title:"状态",items:c,actions:[$("button",{key:"notification",type:"button",className:`notification-icon-btn ${_>0?"has-unread":""}`,onClick:t,"aria-label":"通知"},"\uD83D\uDD14",_>0?$("span",{key:"badge",className:"notification-badge"},_>99?"99+":_):null),$("button",{key:"refresh",type:"button",className:"ghost-btn",onClick:f},"刷新"),$("button",{key:"logout",type:"button",className:"ghost-btn danger",onClick:r},"退出")]}))}function Gp(u){return!u.defaultPrevented&&u.button===0&&!u.metaKey&&!u.altKey&&!u.ctrlKey&&!u.shiftKey&&u.currentTarget.target!=="_blank"}function HL({moduleId:u,tabId:l,className:f,active:r=!1,title:n,testId:i,onNavigate:y,children:t}){let _=yc(Dr,u,l);return $("a",{href:_,role:"button",className:f,title:n,"aria-current":r?"page":void 0,"data-testid":i,"data-route":_,onClick:(c)=>{if(!Gp(c))return;c.preventDefault(),y(u,l)}},t)}function zp({activeModule:u,activeTabs:l,onNavigate:f,collapsed:r,onToggle:n}){return $("aside",{className:`rail ${r?"collapsed":""}`,"aria-label":"主模块"},$("div",{className:"brand"},$("span",{className:"brand-mark"},"UD"),$("span",{className:"brand-text"},"UniDesk"),$("button",{type:"button",className:"rail-toggle",onClick:n,"aria-label":r?"展开左侧边栏":"收起左侧边栏","data-testid":"rail-toggle"},r?"»":"«")),ic.map((i)=>{let y=l[i.id]||st[i.id]||i.tabs[0]?.id||"";return $(HL,{key:i.id,moduleId:i.id,tabId:y,className:`module ${u===i.id?"active":""}`,active:u===i.id,title:i.label,onNavigate:f},$("span",{className:"module-code"},i.code),$("span",null,i.label))}))}function Tp({module:u,activeTab:l,onNavigate:f}){return $("nav",{className:"tabs","aria-label":`${u.label} 子功能`},u.tabs.map((r)=>$(HL,{key:r.id,moduleId:u.id,tabId:r.id,className:`tab ${l===r.id?"active":""}`,active:l===r.id,onNavigate:f},r.label)))}function Ep({data:u,onRaw:l,onNavigate:f}){let r=u.overview||{},n=u.nodes.filter((J)=>J.status==="online"),i=u.pendingTasks||u.tasks.filter(Ly),y=r.pendingTaskCount??i.length,t=u.tasks.slice(0,5),_=r.pgdata||{},c=r.microserviceAvailability||{},A=Mu(c.totalCount),j=Mu(c.healthyCount),F=Mu(c.unhealthyCount);return $("div",{className:"page-grid overview-grid","data-testid":"overview-page"},$(du,{title:"核心指标",eyebrow:"Control"},$("div",{className:"metric-grid"},$(cl,{label:"数据库",value:r.dbReady?"READY":"WAIT",hint:"PostgreSQL internal network",tone:r.dbReady?"ok":"warn"}),$(cl,{label:"PGDATA",value:Tf(_.databaseBytes),hint:`${_.volumeName||"unidesk_pgdata_10gb"} / ${_.databasePretty||"--"}`,tone:"ok",testId:"pgdata-usage-card"}),$(cl,{label:"在线节点",value:r.onlineNodeCount??0,hint:`${r.nodeCount??0} registered`,tone:"ok"}),$(cl,{label:"WebSocket",value:r.activeSocketCount??0,hint:"Provider ingress sockets"}),$(cl,{label:"用户服务可用",value:A>0?`${j}/${A}`:"--",hint:A>0?`healthyCount ${j} · unhealthyCount ${F}`:"strict /health probes",tone:A>0&&F===0?"ok":"warn",testId:"microservice-availability-card"}),$(cl,{label:"待处理任务",value:y,hint:y>0?"点击查看具体任务":`timeout ${b0(Math.floor((r.taskPendingTimeoutMs??0)/1000))}`,tone:y>0?"warn":"ok",onClick:()=>f("tasks","pending"),testId:"pending-task-card"}))),$(du,{title:"本机 Provider",eyebrow:"Self Connected"},n.length===0?$(jl,{title:"暂无在线节点",text:"provider-gateway 未完成自接入"}):$("div",{className:"node-card-list"},n.slice(0,4).map((J)=>$(Zp,{key:J.providerId,node:J,onRaw:l})))),$(du,{title:"待处理任务明细",eyebrow:`${y} Pending`,actions:$("button",{type:"button",className:"ghost-btn",onClick:()=>f("tasks","pending"),"data-testid":"pending-task-detail-link"},"进入任务调度")},i.length===0?$(jl,{title:"当前无待处理",text:"queued / dispatched / running 超时后会自动转为 failed,避免总览长期卡住"}):$("div",{className:"compact-list"},i.slice(0,5).map((J)=>$(FL,{key:J.id,task:J,onRaw:l})))),$(du,{title:"最近任务",eyebrow:"Dispatch"},t.length===0?$(jl,{title:"暂无任务",text:"可以在任务调度模块发起 docker.ps 或 echo"}):$("div",{className:"compact-list"},t.map((J)=>$(FL,{key:J.id,task:J,onRaw:l})))))}function Zp({node:u,onRaw:l}){return $("article",{className:"node-card"},$("div",{className:"node-card-head"},$("div",null,$("strong",null,u.name),$("code",null,u.providerId)),$(wl,{status:u.status})),$("div",{className:"node-version-line"},$(wy,{node:u}),$("span",null,`升级策略 ${h_(u)}`)),$(_7,{node:u}),$(OL,{labels:u.labels,limit:6}),$("div",{className:"node-card-foot"},$("span",null,`心跳 ${qu(u.lastHeartbeat)}`),$(bl,{title:`Provider ${u.providerId}`,data:u,onOpen:l,testId:`raw-node-${ef(u.providerId)}`})))}function Op({events:u,onRaw:l}){return $(du,{title:"事件摘要",eyebrow:"Latest 100"},u.length===0?$(jl,{title:"暂无事件",text:"Provider 注册、心跳超时和任务状态会写入事件流"}):$("div",{className:"table-wrap"},$("table",null,$("thead",null,$("tr",null,$("th",null,"ID"),$("th",null,"类型"),$("th",null,"来源"),$("th",null,"摘要"),$("th",null,"时间"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.id},$("td",null,$("code",null,f.id)),$("td",null,$(wl,{status:f.type},f.type)),$("td",null,$("code",null,f.source)),$("td",null,$(sn,{data:f.payload})),$("td",null,qu(f.createdAt)),$("td",null,$(bl,{title:`Event ${f.id}`,data:f,onOpen:l}))))))))}function Hp({logs:u,onRaw:l}){return $(du,{title:"服务日志",eyebrow:"Core Recent"},u.length===0?$(jl,{title:"暂无日志",text:"backend-core 内存日志会在请求和 provider 事件后出现"}):$("div",{className:"log-list"},u.slice(-80).reverse().map((f,r)=>$("article",{key:r,className:`log-row ${f.level||"info"}`},$("span",null,qu(f.ts)),$("b",null,f.level||"info"),$("strong",null,f.message||"log"),$(sn,{data:f.data,empty:"无附加字段"}),$(bl,{title:`Log ${f.message||r}`,data:f,onOpen:l})))))}function Bp({nodes:u,onRaw:l}){return $(du,{title:"节点清单",eyebrow:`${u.length} Providers`},u.length===0?$(jl,{title:"暂无 Provider 节点",text:"确认 provider-gateway 已连接 provider ingress"}):$("div",{className:"table-wrap"},$("table",{className:"node-list-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"Provider"),$("th",null,"网关版本"),$("th",null,"运维可用性"),$("th",null,"资源标签"),$("th",null,"连接时间"),$("th",null,"最后心跳"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.providerId},$("td",null,$(wl,{status:f.status})),$("td",null,$("strong",null,f.name),$("code",null,f.providerId)),$("td",null,$("div",{className:"gateway-cell"},$(wy,{node:f}),$("span",null,h_(f)))),$("td",null,$(_7,{node:f})),$("td",null,$(OL,{labels:f.labels,limit:5})),$("td",null,qu(f.connectedAt)),$("td",null,qu(f.lastHeartbeat)),$("td",null,$(bl,{title:`Provider ${f.providerId}`,data:f,onOpen:l,testId:`raw-node-table-${ef(f.providerId)}`}))))))))}function Vp({nodes:u}){let l=b_(()=>{let f=[];for(let r of u)for(let[n,i]of WL(r.labels))f.push({providerId:r.providerId,name:r.name,key:n,value:i});return f},[u]);return $(du,{title:"资源标签",eyebrow:"Structured Labels"},l.length===0?$(jl,{title:"暂无标签",text:"provider-gateway 注册消息会同步资源标签"}):$("div",{className:"label-matrix"},l.map((f)=>$("article",{key:`${f.providerId}-${f.key}`,className:"label-card"},$("span",null,f.key),$("strong",null,hi(f.value)),$("code",null,f.providerId)))))}function Dp({nodes:u}){return $(du,{title:"心跳状态",eyebrow:"Provider Liveness"},u.length===0?$(jl,{title:"无心跳",text:"等待 provider 注册和 heartbeat"}):$("div",{className:"heartbeat-list"},u.map((l)=>$("article",{key:l.providerId,className:"heartbeat-row"},$("span",{className:`pulse ${l.status}`}),$("div",null,$("strong",null,l.name),$("code",null,l.providerId)),$("div",null,$("span",null,"connected"),$("b",null,qu(l.connectedAt))),$("div",null,$("span",null,"last heartbeat"),$("b",null,qu(l.lastHeartbeat)))))))}function Xp({nodes:u,systemStatuses:l,tasks:f,onRaw:r,refresh:n}){let[i,y]=bu(""),t=b_(()=>u.map((w)=>{let L=l.find((U)=>U.providerId===w.providerId);return{...w,systemCurrent:L?.current||null,systemHistory:L?.history||[],systemUpdatedAt:L?.updatedAt||null}}),[u,l]),_=t.find((w)=>w.providerId===i)||t[0]||null;if(J0(()=>{if(!i&&t[0])y(t[0].providerId)},[t.length,i]),!_)return $(jl,{title:"暂无资源监控",text:"等待 provider 上报 CPU、内存和硬盘指标"});let c=_.systemCurrent,A=_.systemHistory||[],j=c?.cpu||{},F=c?.memory||{},J=c?.disk||{},Q=A.length>0?A:c?[{at:c.collectedAt,cpuPercent:Mu(j.percent),memoryPercent:Mu(F.percent),diskPercent:Mu(J.percent)}]:[];return $("div",{className:"monitor-page","data-testid":"node-monitor-page"},$("div",{className:"docker-node-strip"},t.map((w)=>$("button",{key:w.providerId,type:"button",className:`docker-node-tile ${_.providerId===w.providerId?"active":""}`,onClick:()=>y(w.providerId)},$("span",{className:`pulse ${w.status}`}),$("strong",null,w.name),$("code",null,w.providerId),$("span",null,w.systemCurrent?`CPU ${In(w.systemCurrent.cpu?.percent)} / MEM ${In(w.systemCurrent.memory?.percent)}`:"等待指标")))),$("div",{className:"monitor-layout"},$(du,{title:"任务管理器视图",eyebrow:_.name,className:"monitor-main-panel",actions:c?$(bl,{title:`System ${_.providerId}`,data:{current:c,history:A},onOpen:r}):null},!c?$(jl,{title:"系统指标未上报",text:"provider-gateway 会周期性采集 /proc 与 df,并保存历史曲线"}):$("div",null,$("div",{className:"monitor-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Node Performance"),$("h3",null,_.name),$("div",{className:"docker-meta"},$("span",null,`${j.cores||0} CPU cores`),$("span",null,`load ${Mu(j.load1).toFixed(2)} / ${Mu(j.load5).toFixed(2)} / ${Mu(j.load15).toFixed(2)}`),$("span",null,`memory actual ${Tf(F.usedBytes)} / ${Tf(F.totalBytes)}`),$("span",null,`disk ${Tf(J.usedBytes)} / ${Tf(J.totalBytes)}`))),$(wl,{status:c.ok?"online":"warn"},c.ok?"METRICS READY":"METRICS DEGRADED")),$("div",{className:"monitor-chart-grid"},$(f7,{title:"CPU",metricKey:"cpuPercent",current:j.percent,points:Q,detail:`${j.cores||0} cores / load ${Mu(j.load1).toFixed(2)}`,tone:"cpu",testId:"metric-chart-cpu"}),$(f7,{title:"Memory",metricKey:"memoryPercent",current:F.percent,points:Q,detail:`${Tf(F.usedBytes)} actual / ${Tf(F.cacheBytes)} cache excluded`,tone:"memory",testId:"metric-chart-memory"}),$(f7,{title:"Disk",metricKey:"diskPercent",current:J.percent,points:Q,detail:`${J.path||"/"} mounted ${J.mount||"--"}`,tone:"disk",testId:"metric-chart-disk"})),$("div",{className:"monitor-summary-grid"},$(cl,{label:"CPU 当前",value:In(j.percent),hint:`history ${Q.length} samples`,tone:"ok"}),$(cl,{label:"实际内存",value:Tf(F.usedBytes),hint:`${In(F.percent)} 不含缓存`}),$(cl,{label:"硬盘已用",value:Tf(J.usedBytes),hint:In(J.percent)}),$(cl,{label:"更新时间",value:qu(_.systemUpdatedAt||c.collectedAt),hint:_.providerId})),$(Sp,{current:c,onRaw:r}))),$("div",{className:"monitor-side-stack"},$(xp,{provider:_,refresh:n,onRaw:r}),$(hp,{provider:_,tasks:f,onRaw:r,limit:5}),$(du,{title:"采样说明",eyebrow:"Retention"},$("div",{className:"monitor-note-list"},$("article",null,$("b",null,"CPU"),$("span",null,"从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),$("article",null,$("b",null,"Memory"),$("span",null,"实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),$("article",null,$("b",null,"Disk"),$("span",null,"使用 df -PB1 对配置路径采样,默认监控根文件系统")),$("article",null,$("b",null,"Process"),$("span",null,"从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")))))))}function cL(u,l){if(l==="memory")return Mu(u.rssBytes);if(l==="cpu")return Mu(u.cpuPercent);if(l==="disk")return Mu(u.readBytesPerSecond)+Mu(u.writeBytesPerSecond);if(l==="pid")return Mu(u.pid);if(l==="threads")return Mu(u.threads);if(l==="runtime")return Mu(u.elapsedSeconds);if(l==="user")return String(u.user||"");return String(u.name||u.command||"")}function AL({value:u,label:l,tone:f}){let r=Math.max(1,Math.min(100,Mu(u)));return $("div",{className:`process-meter ${f||""}`},$("span",{style:{width:`${r}%`}}),$("b",null,l))}function Sp({current:u,onRaw:l}){let[f,r]=bu({key:"memory",direction:"desc"}),n=gn.default.useContext(n7),i=u?.processSummary&&typeof u.processSummary==="object"?u.processSummary:{},y=Array.isArray(u?.processes)?u.processes:[],t=b_(()=>{let c=f.direction==="asc"?1:-1;return[...y].sort((A,j)=>{let F=cL(A,f.key),J=cL(j,f.key);if(typeof F==="string"||typeof J==="string")return String(F).localeCompare(String(J),"zh-CN")*c;return(F-J)*c||Mu(A.pid)-Mu(j.pid)})},[y,f.key,f.direction]),_=(c,A)=>{let j=f.key===A,F=j?f.direction==="asc"?"ascending":"descending":"none";return $("th",{"aria-sort":F},$("button",{type:"button",className:`process-sort-button ${j?"active":""}`,"data-testid":`process-sort-${A}`,onClick:()=>r((J)=>({key:A,direction:J.key===A&&J.direction==="desc"?"asc":"desc"}))},c,$("span",null,j?f.direction==="desc"?"↓":"↑":"↕")))};return $("section",{className:"process-resource-panel","data-testid":"process-resource-panel"},$("div",{className:"process-resource-head"},$("div",null,$("p",{className:"panel-eyebrow"},"Windows Resource Monitor Style"),$(nl,{title:"进程资源占用",level:3,loading:n})),$("div",{className:"process-resource-actions"},$("span",{className:"data-chip"},"默认按内存排序"),$("span",{className:"data-chip"},`${Mu(i.visible,t.length)} / ${Mu(i.total,t.length)} 进程`),$(bl,{title:"Process Resource Snapshot",data:{processSummary:i,processes:y},onOpen:l,testId:"raw-process-resources"}))),t.length===0?$(jl,{title:"暂无进程资源数据",text:"等待 provider-gateway 上报 /proc/[pid] 采样;旧版 provider 需要先升级到支持进程资源表的版本"}):$("div",{className:"process-table-wrap"},$("table",{className:"process-resource-table","data-testid":"process-resource-table"},$("thead",null,$("tr",null,_("进程","name"),_("PID","pid"),_("用户","user"),$("th",null,"状态"),_("CPU","cpu"),_("内存","memory"),$("th",null,"RSS"),_("磁盘 I/O","disk"),_("线程","threads"),_("运行时长","runtime"))),$("tbody",null,t.map((c)=>{let A=Mu(c.readBytesPerSecond)+Mu(c.writeBytesPerSecond);return $("tr",{key:`${c.pid}-${c.startedAt}`,"data-testid":`process-row-${ef(c.pid)}`,"data-memory-bytes":String(Mu(c.rssBytes)),"data-cpu-percent":String(Mu(c.cpuPercent)),"data-disk-bps":String(A),"data-pid":String(Mu(c.pid))},$("td",null,$("div",{className:"process-name-cell"},$("strong",null,c.name||"--"),$("span",{className:"process-command"},c.command||"--"))),$("td",null,$("code",null,c.pid||"--")),$("td",null,c.user||`uid:${c.uid??"--"}`),$("td",null,$("span",{className:`process-state state-${ef(c.state||"unknown")}`},c.state||"?")),$("td",null,$(AL,{value:c.cpuPercent,label:jp(c.cpuPercent),tone:"cpu"})),$("td",null,$(AL,{value:c.memoryPercent,label:In(c.memoryPercent),tone:"memory"})),$("td",null,Tf(c.rssBytes)),$("td",null,$("div",{className:"process-io-cell"},$("strong",null,l7(A)),$("span",null,`R ${l7(c.readBytesPerSecond)} / W ${l7(c.writeBytesPerSecond)}`))),$("td",null,c.threads||0),$("td",null,b0(Mu(c.elapsedSeconds))))})))))}function f7({title:u,metricKey:l,current:f,points:r,detail:n,tone:i,testId:y}){let t=r.map((F)=>Math.max(0,Math.min(100,Mu(F[l])))),_=t.length>1?t:[t[0]||0,t[0]||0],c=_.length<=1?100:100/(_.length-1),A=_.map((F,J)=>`${(J*c).toFixed(2)},${(46-F*0.42).toFixed(2)}`).join(" "),j=`0,48 ${A} 100,48`;return $("article",{className:`metric-chart ${i}`,"data-testid":y},$("div",{className:"metric-chart-head"},$("div",null,$("span",null,u),$("strong",null,In(f))),$("code",null,`${r.length} pts`)),$("svg",{viewBox:"0 0 100 48",preserveAspectRatio:"none",role:"img","aria-label":`${u} usage curve`},$("polygon",{points:j}),$("polyline",{points:A}),$("line",{x1:"0",x2:"100",y1:"24",y2:"24"})),$("div",{className:"metric-chart-foot"},$("span",null,"0%"),$("span",null,n),$("span",null,"100%")))}function v0(u){return Array.isArray(u)?u:[]}function Yp(u){let l=v0(u?.core?.requests?.componentSummary);return[...v0(u?.frontend?.requests?.componentSummary),...l].sort((r,n)=>Mu(n.requestCount)-Mu(r.requestCount))}function pp(u){let l=v0(u?.core?.operations?.summary);return[...v0(u?.frontend?.operations?.summary),...l].sort((r,n)=>Mu(n.count)-Mu(r.count))}function mp(u){let l=v0(u?.core?.requests?.recentFailures).map((r)=>({source:"backend",...r}));return[...v0(u?.frontend?.requests?.recentFailures).map((r)=>({source:"frontend",...r})),...l].sort((r,n)=>(Q0(n.at)??0)-(Q0(r.at)??0)).slice(0,20)}function Pp(u){let l=v0(u?.core?.operations?.recentSlowOperations);return[...v0(u?.frontend?.operations?.recentSlowOperations),...l].sort((r,n)=>Mu(n.durationMs)-Mu(r.durationMs)).slice(0,20)}function Cp(u){let l=performance.memory,f=Number(l?.usedJSHeapSize);if(Number.isFinite(f)&&f>0)return f;let r=Number(u?.appBundleBytes);if(Number.isFinite(r)&&r>0)return r;return Mu(u?.process?.heapUsedBytes)}function Mp({points:u}){let l=v0(u),f=l.map((F)=>Mu(F.mb)),r=Math.max(1,...f),n=Math.max(0,Math.min(...f,0)),i=Math.max(1,r-n),y=l.length>1?l:[...l,...l],t=y.length<=1?100:100/(y.length-1),_=y.map((F,J)=>{let Q=Mu(F.mb);return`${(J*t).toFixed(2)},${(48-(Q-n)/i*42).toFixed(2)}`}).join(" "),c=`0,50 ${_} 100,50`,A=l.at(-1),j=l[0];return $("article",{className:"performance-memory-card","data-testid":"performance-memory-chart"},$("div",{className:"performance-memory-head"},$("strong",null,`Bwebui: ${A?`${Mu(A.mb).toFixed(1)}MB`:"--"}`),$("span",null,l.length>0?`${l.length} samples`:"等待采样")),$("svg",{viewBox:"0 0 100 50",preserveAspectRatio:"none",role:"img","aria-label":"Bwebui memory trend"},$("polygon",{points:c}),$("polyline",{points:_}),$("line",{x1:"0",x2:"100",y1:"25",y2:"25"})),$("div",{className:"performance-axis-row"},$("span",null,j?tl(new Date(j.at)):"--"),$("span",null,"时间"),$("span",null,A?tl(new Date(A.at)):"--")),$("div",{className:"performance-axis-row"},$("span",null,`${n.toFixed(1)}`),$("span",null,"(MB)"),$("span",null,`${r.toFixed(1)}`)))}function Rp({onRaw:u}){let[l,f]=bu({core:null,frontend:null}),[r,n]=bu([]),[i,y]=bu(""),[t,_]=bu(!1),[c,A]=bu(null),[j,F]=bu(!1);async function J(){_(!0),y("");try{let[S,p]=await Promise.all([Tu(`${gu.apiBaseUrl}/performance`,{cache:"no-store"}),Tu(`${gu.apiBaseUrl}/frontend-performance`,{cache:"no-store"})]);f({core:S,frontend:p});let O=Cp(p);n((m)=>[...m,{at:new Date().toISOString(),mb:O/1048576}].slice(-80))}catch(S){y(Ou(S,"性能指标加载失败"))}finally{_(!1)}}J0(()=>{J();let S=setInterval(()=>void J(),5000);return()=>clearInterval(S)},[]);async function Q(){F(!0),y(""),A(null);try{let S=await Tu(`${gu.apiBaseUrl}/code-queue-load-test`,{method:"POST",body:JSON.stringify({targetMs:1000,timeoutMs:90000,url:gu.frontendPublicUrl||window.location.origin})});A(S),J()}catch(S){y(Ou(S,"Code Queue Playwright 测量失败"))}finally{F(!1)}}let w=Yp(l),L=mp(l),U=pp(l),N=Pp(l),q=l.core?.process||{},W=l.frontend?.process||{},z=l.core?.database?.codeQueueStorage||{},Z=Mu(z.total),H=c?.result||{},E=Mu(H.wallMs,NaN),D=Mu(H.networkIdleMs,NaN),h=H.withinTarget===!0,V=j?"running":c===null?"idle":c.measurementOk===!0?h?"passed":"slow":"failed";return $("div",{className:"performance-page","data-testid":"performance-page"},$("div",{className:"performance-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Unified Performance"),$(nl,{title:"性能面板",loading:t||j}),$("p",null,"按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。")),$("div",{className:"inline-actions"},$("button",{type:"button",className:"ghost-btn",onClick:()=>void Q(),disabled:j,"data-testid":"code-queue-load-test-button"},j?"测试中...":"测试 Code Queue 加载"),$("button",{type:"button",className:"ghost-btn",onClick:()=>void J(),disabled:t,"data-testid":"performance-refresh-button"},t?"刷新中":"刷新"),$(bl,{title:"Performance Snapshot",data:l,onOpen:u,testId:"raw-performance"}))),$(il,{error:i}),$("div",{className:"performance-top-grid"},$(Mp,{points:r}),$("div",{className:"performance-metric-stack"},$(cl,{label:"backend RSS",value:Tf(q.rssBytes),hint:`heap ${Tf(q.heapUsedBytes)}`}),$(cl,{label:"frontend RSS",value:Tf(W.rssBytes),hint:`bundle ${Tf(l.frontend?.appBundleBytes)}`}),$(cl,{label:"Codex PG 任务",value:Z||"--",hint:z.ok?"unidesk_code_queue_tasks":"等待表初始化",tone:z.ok?"ok":"warn"}),$(cl,{label:"请求样本",value:Mu(l.core?.requests?.sampleCount)+Mu(l.frontend?.requests?.sampleCount),hint:"rolling window 3000"}))),$(du,{title:"Code Queue 加载基准",eyebrow:"Playwright / target <1s",className:"codex-load-test-panel",loading:j,actions:$("div",{className:"panel-actions"},$("button",{type:"button",className:"primary-btn",onClick:()=>void Q(),disabled:j,"data-testid":"code-queue-load-test-panel-button"},j?"正在运行 Playwright...":"手动触发测试"),c?$(bl,{title:"Code Queue Load Test",data:c,onOpen:u,testId:"raw-code-queue-load-test"}):null)},$("div",{className:"codex-load-test-grid","data-testid":"code-queue-load-test-result"},$(cl,{label:"总耗时",value:j?"运行中":Number.isFinite(E)?Nr(E):"--",hint:c===null?"点击按钮启动远端 Playwright":`目标 ${Nr(H.targetMs||1000)} / ${H.url||"Code Queue"}`,tone:V==="passed"?"ok":V==="failed"||V==="slow"?"warn":""}),$(cl,{label:"判定",value:j?"RUNNING":V==="passed"?"PASS <1s":V==="slow"?"SLOW":V==="failed"?"FAILED":"--",hint:c?.measurementOk===!1?String(c.error||H.error||"measurement failed").slice(0,120):"导航开始 -> DOMContentLoaded -> data-load-state=complete",tone:V==="passed"?"ok":V==="idle"||V==="running"?"":"fail"}),$(cl,{label:"Network idle",value:Number.isFinite(D)?Nr(D):"--",hint:`DOMContentLoaded ${Nr(H.domContentLoadedMs)} / ${H.networkIdleReached===!1?"未在 5s 内空闲":"已空闲"}`,tone:Number.isFinite(D)&&D<=1000?"ok":"warn"}),$(cl,{label:"组件耗时",value:Number.isFinite(Mu(H.componentLoadMs,NaN))?Nr(H.componentLoadMs):"--",hint:`queue ${Nr(H.queueMs)} / detail ${Nr(H.detailMs)}`,tone:Mu(H.componentLoadMs)>1000?"warn":"ok"}),$(cl,{label:"Trace 规模",value:Number.isFinite(Mu(H.transcriptRows,NaN))?String(H.transcriptRows):"--",hint:`${H.visibleTaskCount??0} visible tasks / ${H.partial?"preview":"complete"}`})),j?$("div",{className:"performance-empty-line"},"正在通过 main-server Host SSH 启动 Playwright,完成后会显示 wall time、组件耗时和最慢 API。"):null,c&&Array.isArray(H.slowestApi)&&H.slowestApi.length>0?$("div",{className:"table-wrap performance-table-wrap compact codex-load-api-table"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["API","状态","耗时"].map((S)=>$("th",{key:S},S)))),$("tbody",null,H.slowestApi.slice(0,5).map((S,p)=>$("tr",{key:`${S.url}-${p}`},$("td",null,$("code",null,S.url)),$("td",null,S.status),$("td",null,Nr(S.durationMs))))))):null),$("div",{className:"performance-grid"},$(du,{title:"组件汇总",eyebrow:"Requests",loading:t},w.length===0?$(jl,{title:"暂无请求样本",text:"刷新几次或打开页面后会自动形成组件统计"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["组件","请求数","失败数","失败率","平均延迟","P95"].map((S)=>$("th",{key:S},S)))),$("tbody",null,w.map((S)=>$("tr",{key:S.component},$("td",null,$("code",null,S.component)),$("td",null,S.requestCount),$("td",null,S.failureCount),$("td",null,In(Mu(S.failureRate)*100)),$("td",null,Nr(S.averageLatencyMs)),$("td",null,Nr(S.p95LatencyMs)))))))),$(du,{title:"最近失败请求",eyebrow:"Failures",loading:t},L.length===0?$("div",{className:"performance-empty-line"},"最近没有失败请求"):$("div",{className:"table-wrap performance-table-wrap compact"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["时间","来源","组件","状态","路径"].map((S)=>$("th",{key:S},S)))),$("tbody",null,L.map((S,p)=>$("tr",{key:`${S.at}-${p}`},$("td",null,qu(S.at)),$("td",null,S.source),$("td",null,$("code",null,S.component)),$("td",null,$(wl,{status:"failed"},S.status)),$("td",null,$("code",null,S.path)))))))),$(du,{title:"内部操作汇总",eyebrow:"Operations",loading:t},U.length===0?$(jl,{title:"暂无内部操作样本",text:"API 查询和代理请求会自动记录内部操作耗时"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["服务","操作","次数","平均延迟","P95"].map((S)=>$("th",{key:S},S)))),$("tbody",null,U.map((S)=>$("tr",{key:`${S.service}-${S.operation}`},$("td",null,S.service),$("td",null,$("code",null,S.operation)),$("td",null,S.count),$("td",null,Nr(S.averageLatencyMs)),$("td",null,Nr(S.p95LatencyMs)))))))),$(du,{title:"最近慢操作",eyebrow:"Slowest",loading:t},N.length===0?$(jl,{title:"暂无慢操作",text:"后端会记录最近窗口内耗时最高的内部操作"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["时间","操作","耗时","结果","细节"].map((S)=>$("th",{key:S},S)))),$("tbody",null,N.map((S,p)=>$("tr",{key:`${S.at}-${S.operation}-${p}`},$("td",null,qu(S.at)),$("td",null,$("code",null,S.operation)),$("td",null,Nr(S.durationMs)),$("td",null,S.ok?"成功":"失败"),$("td",null,S.detail||"-")))))))))}function xp({provider:u,refresh:l,onRaw:f}){let[r,n]=bu(""),[i,y]=bu(null),[t,_]=bu("");async function c(A){n(A),_("");try{let j=await Tu(`${gu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:u.providerId,command:"provider.upgrade",payload:{mode:A,source:"frontend-resource-monitor",requestedAt:new Date().toISOString()}})});y({mode:A,...j}),await l()}catch(j){_(Ou(j,"升级命令下发失败"))}finally{n("")}}return $(du,{title:"Provider Gateway 升级",eyebrow:"Remote Control",loading:Boolean(r)},$("div",{className:"upgrade-control","data-testid":"provider-upgrade-control"},$("p",null,"通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"),$("div",{className:"upgrade-target-line"},$("span",null,"指定 Provider"),$("code",null,u.providerId),$(wy,{node:u})),$("div",{className:"upgrade-actions"},$("button",{type:"button",className:"ghost-btn",disabled:Boolean(r),onClick:()=>c("plan"),"data-testid":"upgrade-plan-button"},r==="plan"?"预检中":"预检升级"),$("button",{type:"button",className:"ghost-btn danger",disabled:Boolean(r),onClick:()=>c("schedule"),"data-testid":"upgrade-schedule-button"},r==="schedule"?"调度中":"执行升级")),$(il,{error:t}),i?$("div",{className:"upgrade-result"},$(wl,{status:i.status||"queued"},i.status||"queued"),$("span",null,`${i.mode==="schedule"?"执行升级":"预检升级"} 已下发`),$("span",null,`指定版本 ${t7(wL(u))}`),$("code",null,i.taskId||"--"),$(bl,{title:"Provider Upgrade Dispatch",data:i,onOpen:f})):$("span",{className:"muted"},"升级任务结果会进入任务历史;执行升级可能导致 provider 短暂重连。")))}function BL({records:u,onRaw:l,compact:f=!1}){if(u.length===0)return $(jl,{title:"暂无远程更新记录",text:"该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录"});return $("div",{className:`upgrade-record-table-wrap table-wrap ${f?"compact":""}`},$("table",{className:"upgrade-record-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"模式"),$("th",null,"任务"),$("th",null,"来源"),$("th",null,"耗时"),$("th",null,"策略"),$("th",null,"Gateway 版本"),$("th",null,"结果记录"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,u.map((r)=>$("tr",{key:r.id,"data-testid":`gateway-upgrade-record-${ef(r.id)}`},$("td",null,$(wl,{status:r.status})),$("td",null,$("span",{className:`mode-chip ${S6(r)}`},S6(r)==="schedule"?"执行升级":"预检")),$("td",null,$("strong",null,"provider.upgrade"),$("code",null,r.id)),$("td",null,Qp(r)),$("td",null,$(DL,{task:r})),$("td",null,Np(r)),$("td",null,$("span",{className:"version-chip"},zL(r))),$("td",null,$("span",{className:`upgrade-outcome ${String(r.status||"").toLowerCase()}`},TL(r))),$("td",null,qu(r.updatedAt)),$("td",null,$(bl,{title:`Provider Upgrade Task ${r.id}`,data:v_(r),onOpen:l})))))))}function hp({provider:u,tasks:l,onRaw:f,limit:r=5}){let n=EL(l,u.providerId).slice(0,r);return $(du,{title:"远程更新记录",eyebrow:u.providerId,actions:$(wy,{node:u}),className:"provider-upgrade-records-panel"},$("div",{"data-testid":`provider-upgrade-records-${ef(u.providerId)}`},$(BL,{records:n,onRaw:f,compact:!0})))}function bp({nodes:u,tasks:l,onRaw:f}){let r=b_(()=>u.map((i)=>{let y=EL(l,i.providerId);return{node:i,records:y,latest:qp(y),capabilities:LL(i)}}),[u,l]),n=r.reduce((i,y)=>i+y.records.length,0);return $("div",{className:"gateway-page","data-testid":"gateway-version-page"},$(du,{title:"Provider Gateway 版本",eyebrow:`${u.length} Providers / ${n} 更新记录`},u.length===0?$(jl,{title:"暂无 Provider 节点",text:"等待 provider-gateway 注册后显示版本号和升级记录"}):$("div",{className:"table-wrap gateway-version-table-wrap"},$("table",{className:"gateway-version-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"Provider"),$("th",null,"Gateway 版本"),$("th",null,"升级策略"),$("th",null,"运维可用性"),$("th",null,"运行时间"),$("th",null,"能力"),$("th",null,"最近远程更新"),$("th",null,"操作"))),$("tbody",null,r.map((i)=>$("tr",{key:i.node.providerId},$("td",null,$(wl,{status:i.node.status})),$("td",null,$("strong",null,i.node.name),$("code",null,i.node.providerId)),$("td",null,$(wy,{node:i.node})),$("td",null,h_(i.node)),$("td",null,$(_7,{node:i.node})),$("td",null,yL(i.node)?qu(yL(i.node)):"待新版上报"),$("td",null,$("div",{className:"capability-row"},i.capabilities.length===0?$("span",{className:"muted"},"未声明"):i.capabilities.slice(0,5).map((y)=>$("span",{key:y,className:"data-chip"},y)))),$("td",null,i.latest?$("div",{className:"latest-upgrade-cell"},$(wl,{status:i.latest.status}),$("span",null,`${S6(i.latest)==="schedule"?"执行升级":"预检"} / ${qu(i.latest.updatedAt)}`),$("small",null,`Gateway ${zL(i.latest)}`),$("small",null,TL(i.latest))):$("span",{className:"muted"},"暂无记录")),$("td",null,$(bl,{title:`Provider ${i.node.providerId}`,data:i.node,onOpen:f})))))))),$(du,{title:"远程更新记录",eyebrow:"Structured provider.upgrade records"},u.length===0?$(jl,{title:"暂无记录",text:"没有 provider 节点时不会生成远程更新记录"}):$("div",{className:"gateway-record-grid"},r.map((i)=>$("article",{key:i.node.providerId,className:"gateway-record-card","data-testid":`gateway-records-${ef(i.node.providerId)}`},$("div",{className:"gateway-record-head"},$("div",null,$("strong",null,i.node.name),$("code",null,i.node.providerId)),$(wy,{node:i.node})),$("div",{className:"gateway-record-meta"},$("span",null,`心跳 ${qu(i.node.lastHeartbeat)}`),$("span",null,`策略 ${h_(i.node)}`),$("span",null,`${i.records.length} 条记录`)),$(BL,{records:i.records.slice(0,8),onRaw:f,compact:!0}))))))}function vp(u){if(u==="running")return"online";if(u==="paused"||u==="restarting")return"warn";if(u==="exited"||u==="dead")return"offline";return"internal"}function VL(u){return/^[a-f0-9]{48,64}$/i.test(u)}function x_(u){let l=String(u?.name||""),f=String(u?.labels||"");return l==="unidesk_pgdata_10gb"||f.includes("com.docker.compose.volume=unidesk_pgdata_10gb")||l.toLowerCase().includes("pgdata")}function jL(u){let l=String(u?.name||""),f=String(u?.labels||"");if(x_(u))return 0;if(f.includes("com.docker.compose.project=unidesk"))return 1;if(!VL(l))return 2;return 3}function kp(u){return[...u].sort((l,f)=>{let r=jL(l)-jL(f);if(r!==0)return r;return String(l.name||"").localeCompare(String(f.name||""))})}function Ip({nodes:u,dockerStatuses:l,onRaw:f}){let[r,n]=bu(""),i=b_(()=>u.map((N)=>{let q=l.find((W)=>W.providerId===N.providerId);return{...N,dockerStatus:q?.dockerStatus||null,dockerUpdatedAt:q?.updatedAt||null}}),[u,l]),y=i.find((N)=>N.providerId===r)||i[0]||null;if(J0(()=>{if(!r&&i[0])n(i[0].providerId)},[i.length,r]),!y)return $(jl,{title:"暂无 Docker 节点",text:"等待 provider 上报 Docker daemon 状态"});let t=y.dockerStatus,_=y.providerId==="main-server",c=t?.counts||{},A=t?.daemon||{},j=t?.containers||[],F=t?.images||[],J=kp(t?.volumes||[]),Q=_?J.find(x_):null,w=t?.networks||[],L=j.filter((N)=>N.state==="running"),U=j.filter((N)=>N.state!=="running");return $("div",{className:"docker-page","data-testid":"docker-status-page"},$("div",{className:"docker-node-strip"},i.map((N)=>$("button",{key:N.providerId,type:"button",className:`docker-node-tile ${y.providerId===N.providerId?"active":""}`,onClick:()=>n(N.providerId)},$("span",{className:`pulse ${N.status}`}),$("strong",null,N.name),$("code",null,N.providerId),$("span",null,N.dockerStatus?`Docker ${N.dockerStatus.ok?"ready":"degraded"}`:"等待上报")))),$("div",{className:"docker-layout"},$(du,{title:"Docker Desktop 视图",eyebrow:y.name,className:"docker-main-panel",actions:t?$(bl,{title:`Docker ${y.providerId}`,data:t,onOpen:f}):null},!t?$(jl,{title:"Docker 状态未上报",text:"provider-gateway 会在连接后周期性采集 docker info / ps / images / volume / network"}):$("div",null,$("div",{className:"docker-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Daemon"),$("h3",null,A.name||y.providerId),$("div",{className:"docker-meta"},$("span",null,A.serverVersion?`Engine ${A.serverVersion}`:"Engine --"),$("span",null,A.operatingSystem||"OS --"),$("span",null,A.architecture||"arch --"),$("span",null,`${A.cpus||0} CPU / ${Tf(A.memoryBytes)}`))),$(wl,{status:t.ok?"online":"warn"},t.ok?"Docker Ready":"Docker Degraded")),$("div",{className:"docker-metrics"},$(cl,{label:"Containers",value:c.containers??j.length,hint:`${c.running??L.length} running / ${c.stopped??U.length} stopped`,tone:"ok"}),$(cl,{label:"Images",value:c.images??F.length,hint:`${c.daemonImages??c.images??F.length} daemon images`}),$(cl,{label:"Volumes",value:c.volumes??J.length,hint:_?Q?"database volume visible":"database volume missing":"node local volumes",tone:Q?"ok":""}),$(cl,{label:"Networks",value:c.networks??w.length,hint:A.driver?`driver ${A.driver}`:"docker networks"})),_?$(gp,{volume:Q,volumeCount:J.length}):null,$("div",{className:"docker-section-head"},$("h3",null,"Containers"),$("span",null,`updated ${qu(y.dockerUpdatedAt||t.collectedAt)}`)),$("div",{className:"docker-container-table table-wrap","data-testid":"docker-container-table"},$("table",null,$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"容器"),$("th",null,"镜像"),$("th",null,"端口"),$("th",null,"运行时间"),$("th",null,"重启策略"),$("th",null,"PID"),$("th",null,"大小"))),$("tbody",null,j.length===0?$("tr",null,$("td",{colSpan:8},"暂无容器")):j.map((N)=>$("tr",{key:`${N.id}-${N.name}`},$("td",null,$(wl,{status:vp(N.state)},N.state||"unknown")),$("td",null,$("strong",null,N.name||"--"),$("code",null,N.id||"--")),$("td",null,N.image||"--"),$("td",null,N.ports||$("span",{className:"muted"},"未发布")),$("td",null,N.runningFor||N.status||"--"),$("td",null,N.restartPolicy?$(wl,{status:N.restartPolicy==="always"?"online":"warn"},N.restartPolicy):"--"),$("td",null,N.pidMode?$("code",null,N.pidMode):"--"),$("td",null,N.size||"--")))))))),$("div",{className:"docker-side-stack"},$(r7,{title:"Images",items:F,render:(N)=>$("article",{key:`${N.id}-${N.repository}`,className:"docker-side-row"},$("strong",null,`${N.repository}:${N.tag}`),$("span",null,N.size||"--"),$("code",null,N.id||"--"))}),$(r7,{title:"Volumes",items:J,limit:J.length,render:(N)=>$("article",{key:N.name,className:`docker-side-row volume-row ${_&&x_(N)?"database-volume":""}`,"data-testid":_&&x_(N)?"database-volume-row":void 0},$("strong",null,N.name),$("span",null,_&&x_(N)?"PostgreSQL":VL(String(N.name||""))?"anonymous":"named"),$("code",null,N.mountpoint||N.driver||N.scope||"--"))}),$(r7,{title:"Networks",items:w,render:(N)=>$("article",{key:N.id||N.name,className:"docker-side-row"},$("strong",null,N.name),$("span",null,N.driver||"--"),$("code",null,N.id||"--"))}))))}function gp({volume:u,volumeCount:l}){return $("section",{className:`docker-volume-focus ${u?"ready":"missing"}`,"data-testid":"database-volume-card"},$("div",{className:"volume-focus-head"},$("span",{className:"panel-eyebrow"},"Database Named Volume"),$(wl,{status:u?"online":"warn"},u?"FOUND":"MISSING")),u?$("div",{className:"volume-focus-body"},$("strong",null,u.name),$("span",null,"PostgreSQL data volume for unidesk-database"),$("div",{className:"volume-route"},$("code",null,u.mountpoint||"/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),$("span",null,"->"),$("code",null,"unidesk-database:/var/lib/postgresql/data")),$("div",{className:"docker-meta compact"},$("span",null,`driver ${u.driver||"--"}`),$("span",null,`scope ${u.scope||"--"}`),$("span",null,`${l} volumes reported`))):$("div",{className:"volume-focus-body"},$("strong",null,"unidesk_pgdata_10gb"),$("span",null,"当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。")))}function r7({title:u,items:l,render:f,limit:r}){let n=l.slice(0,r??12),i=Math.max(0,l.length-n.length);return $(du,{title:u,eyebrow:`${l.length} items`,className:"docker-side-panel"},l.length===0?$(jl,{title:`暂无 ${u}`,text:"等待 Docker 状态采集"}):$("div",{className:"docker-side-list"},n.map(f),i>0?$("div",{className:"docker-side-more"},`+ ${i} more`):null))}function sp({microservices:u,onRaw:l,onNavigate:f}){let r=u.filter((n)=>_L(n).public===!1);return $("div",{className:"microservice-page","data-testid":"microservice-catalog-page"},$(du,{title:"用户服务目录",eyebrow:"Provider Mounted User Services"},$("div",{className:"metric-grid"},$(cl,{label:"服务总数",value:u.length,hint:"config.json 用户服务登记"}),$(cl,{label:"私有后端",value:r.length,hint:"不直接暴露公网",tone:"ok"}),$(cl,{label:"D601 服务",value:u.filter((n)=>n.providerId==="D601").length,hint:"compute-node docker"}),$(cl,{label:"集成前端",value:u.filter((n)=>n.frontend?.integrated).length,hint:"UniDesk React 页面"}))),$(du,{title:"服务映射",eyebrow:"Repo Reference + Runtime"},u.length===0?$(jl,{title:"暂无用户服务",text:"在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射"}):$("div",{className:"table-wrap"},$("table",{className:"microservice-table"},$("thead",null,$("tr",null,$("th",null,"服务"),$("th",null,"Provider"),$("th",null,"代码引用"),$("th",null,"Docker 引用"),$("th",null,"后端映射"),$("th",null,"开发入口"),$("th",null,"运行态"),$("th",null,"操作"))),$("tbody",null,u.map((n)=>{let i=ZL(n),y=Wp(n),t=_L(n),_=i.availability||{},c=_.status||(i.providerStatus==="online"?"unknown":"unhealthy");return $("tr",{key:n.id,"data-testid":`microservice-row-${ef(n.id)}`},$("td",null,$("strong",null,n.name),$("code",null,n.id)),$("td",null,$("strong",null,i.providerName||n.providerId),$("code",null,n.providerId)),$("td",null,$("span",null,y.url||"--"),$("code",null,y.commitId||"--")),$("td",null,$("span",null,y.composeFile||"--"),$("code",null,`${y.composeService||"--"} / ${y.containerName||"--"}`)),$("td",null,$(wl,{status:t.public?"warn":"online"},t.public?"public":"private"),$("code",null,`${t.nodeBindHost||"--"}:${t.nodePort||"--"} -> ${t.proxyMode||"--"}`)),$("td",null,$("span",null,n.development?.sshPassthrough?"SSH 透传":"未配置"),$("code",null,n.development?.worktreePath||"--")),$("td",null,$(wl,{status:c==="healthy"?"online":c==="unknown"?"warn":"failed"},c),$("span",null,_.reason||i.providerStatus||"unknown"),$(sn,{data:i.container,empty:"容器快照未上报"})),$("td",null,$("div",{className:"microservice-actions"},n.id==="findjob"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","findjob"),"data-testid":"open-findjob-button"},"打开"):null,n.id==="pipeline"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","pipeline"),"data-testid":"open-pipeline-button"},"打开"):null,n.id==="todo-note"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","todo-note"),"data-testid":"open-todo-note-button"},"打开"):null,n.id==="met-nonlinear"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","met-nonlinear"),"data-testid":"open-met-nonlinear-button"},"打开"):null,n.id==="claudeqq"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","claudeqq"),"data-testid":"open-claudeqq-button"},"打开"):null,n.id==="baidu-netdisk"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","baidu-netdisk"),"data-testid":"open-baidu-netdisk-button"},"打开"):null,n.id==="oa-event-flow"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","oa-event-flow"),"data-testid":"open-oa-event-flow-button"},"打开"):null,n.id==="v3sctl-adapter"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","v3sctl"),"data-testid":"open-v3sctl-button"},"打开"):null,n.id==="code-queue"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","code-queue"),"data-testid":"open-code-queue-button"},"打开"):null,n.id==="project-manager"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","project-manager"),"data-testid":"open-project-manager-button"},"打开"):null,$(bl,{title:`用户服务 ${n.id}`,data:n,onOpen:l}))))}))))))}function ap({nodes:u,onDispatched:l,onRaw:f}){let r=u.filter((V)=>V.status==="online"),[n,i]=bu(r[0]?.providerId||u[0]?.providerId||""),[y,t]=bu("docker.ps"),[_,c]=bu("frontend"),[A,j]=bu("operator-check"),[F,J]=bu("normal"),[Q,w]=bu(!1),[L,U]=bu(""),[N,q]=bu(!1),[W,z]=bu(null),[Z,H]=bu("");J0(()=>{if(!n&&(r[0]?.providerId||u[0]?.providerId))i(r[0]?.providerId||u[0].providerId)},[u.length,r.length,n]);function E(){return{source:_,note:A,priority:F}}function D(){U(JSON.stringify(E(),null,2)),w(!0)}async function h(V){V.preventDefault(),q(!0),H("");try{let S=Q?JSON.parse(L||"{}"):E(),p=await Tu(`${gu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:n,command:y,payload:S})});z(p),await l()}catch(S){H(Ou(S,"下发失败"))}finally{q(!1)}}return $("div",{className:"page-grid dispatch-grid"},$(du,{title:"下发任务",eyebrow:"Real WebSocket Dispatch"},$("form",{className:"dispatch-form",onSubmit:h},$("label",null,"Provider",$("select",{value:n,onChange:(V)=>i(V.target.value)},u.map((V)=>$("option",{key:V.providerId,value:V.providerId},`${V.name} / ${V.providerId}`)))),$("label",null,"Command",$("select",{value:y,onChange:(V)=>t(V.target.value)},$("option",{value:"docker.ps"},"docker.ps"),$("option",{value:"host.ssh"},"host.ssh"),$("option",{value:"microservice.http"},"microservice.http"),$("option",{value:"echo"},"echo"))),$("label",null,"来源",$("input",{value:_,onChange:(V)=>c(V.target.value)})),$("label",null,"备注",$("input",{value:A,onChange:(V)=>j(V.target.value)})),$("label",null,"优先级",$("select",{value:F,onChange:(V)=>J(V.target.value)},$("option",{value:"normal"},"normal"),$("option",{value:"low"},"low"),$("option",{value:"urgent"},"urgent"))),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",onClick:D},"查看原始JSON"),$("button",{type:"submit",disabled:N||!n},N?"下发中":"下发任务")),Q?$("label",{className:"raw-editor-label"},"高级 Payload",$("textarea",{className:"raw-editor",value:L,onChange:(V)=>U(V.target.value)})):null,$(il,{error:Z,wide:!0}))),$(du,{title:"下发结果",eyebrow:"Response"},W?$("div",{className:"result-card"},$(wl,{status:W.status||"queued"},W.status||"queued"),$("dl",null,$("dt",null,"Task ID"),$("dd",null,$("code",null,W.taskId||"--")),$("dt",null,"Provider 在线"),$("dd",null,hi(W.providerOnline))),$(bl,{title:"Dispatch Response",data:W,onOpen:f})):$(jl,{title:"等待操作",text:"任务响应会以结构化结果卡展示"})))}function FL({task:u,onRaw:l}){return $("article",{className:"compact-row"},$(wl,{status:u.status}),$("div",null,$("strong",null,u.command),$("code",null,u.id)),$("span",null,Ly(u)?`已等待 ${i7(u.updatedAt)}`:`耗时 ${b0(NL(u)??0)}`),$(bl,{title:`Task ${u.id}`,data:v_(u),onOpen:l}))}function DL({task:u}){let l=NL(u),f=Ly(u);return $("div",{className:"task-duration"},$("strong",null,l===null?"--":b0(l)),$("span",null,f?`已运行 / 创建 ${qu(u.createdAt)}`:`创建 ${qu(u.createdAt)}`))}function op({task:u}){let l=String(u?.status||"").toLowerCase(),f=u?.result,r=f&&typeof f==="object"&&!Array.isArray(f)?f:{},i=["exitCode","code","signal","timeoutMs","previousStatus","mode"].filter((y)=>r[y]!==void 0&&r[y]!==null);if(l==="failed"){let y=qL(u);return $("div",{className:"task-diagnostic failed"},$("b",null,"失败原因"),$("span",{className:"diagnostic-reason"},hi(y)),i.length>0?$("div",{className:"diagnostic-meta"},i.map((t)=>$("span",{key:t,className:"data-chip"},$("b",null,t),$("span",null,hi(r[t]))))):null)}if(Ly(u))return $("div",{className:"task-diagnostic warn"},$("b",null,"等待终态"),$("span",null,`最后更新 ${i7(u.updatedAt)} 前`));return $("div",{className:"task-diagnostic ok"},$("b",null,"完成摘要"),$(sn,{data:f,empty:"无执行输出"}))}function dp({tasks:u,onRaw:l}){let f=u.filter(Ly);return $("div",{"data-testid":"pending-task-page"},$(du,{title:"待处理任务",eyebrow:`${f.length} Pending`},f.length===0?$(jl,{title:"当前无待处理任务",text:"queued / dispatched / running 会在超时后自动转为 failed;历史记录仍可在任务历史中查看"}):$("div",{className:"table-wrap","data-testid":"pending-task-table"},$("table",null,$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"Provider"),$("th",null,"已等待"),$("th",null,"载荷摘要"),$("th",null,"操作"))),$("tbody",null,f.map((r)=>$("tr",{key:r.id},$("td",null,$(wl,{status:r.status})),$("td",null,$("strong",null,r.command),$("code",null,r.id)),$("td",null,$("code",null,r.providerId)),$("td",null,i7(r.updatedAt)),$("td",null,$(sn,{data:r.payload})),$("td",null,$(bl,{title:`Pending Task ${r.id}`,data:v_(r),onOpen:l})))))))))}function ep({tasks:u,onRaw:l}){return $("div",{"data-testid":"task-history-page"},$(du,{title:"任务历史",eyebrow:`${u.length} Tasks`},u.length===0?$(jl,{title:"暂无任务",text:"下发任务后会在这里看到生命周期"}):$("div",{className:"table-wrap"},$("table",{className:"task-history-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"Provider"),$("th",null,"任务耗时"),$("th",null,"载荷摘要"),$("th",null,"诊断信息"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.id,"data-testid":`task-row-${ef(f.id)}`},$("td",null,$(wl,{status:f.status})),$("td",null,$("strong",null,f.command),$("code",null,f.id)),$("td",null,$("code",null,f.providerId)),$("td",null,$(DL,{task:f})),$("td",null,$(sn,{data:f.payload})),$("td",null,$(op,{task:f})),$("td",null,qu(f.updatedAt)),$("td",null,$(bl,{title:`Task ${f.id}`,data:v_(f),onOpen:l})))))))))}function um({tasks:u,onRaw:l}){let f=u.filter((r)=>["succeeded","failed"].includes(r.status));return $(du,{title:"执行结果",eyebrow:"Finished Tasks"},f.length===0?$(jl,{title:"暂无结果",text:"任务完成后展示 provider 返回的结构化摘要"}):$("div",{className:"result-grid"},f.map((r)=>$("article",{key:r.id,className:"result-card"},$("div",{className:"node-card-head"},$("strong",null,r.command),$(wl,{status:r.status})),$("code",null,r.id),$(sn,{data:r.result,empty:"无执行输出"}),$(bl,{title:`Task Result ${r.id}`,data:v_(r),onOpen:l})))))}function lm(u){if(!u||typeof u!=="object")return"--";if(u.type==="interval")return`每 ${b0(Number(u.everySeconds||0))}`;return`每天 ${u.timeOfDay||"03:00"} UTC`}function fm(u){if(!u||typeof u!=="object")return"--";if(u.type==="pgdata_backup")return`PGDATA -> ${u.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA"}`;if(u.type==="dispatch")return`${u.providerId||"--"} / ${u.command||"--"}`;return String(u.type||"--")}function rm(u){let l=String(u||"").toLowerCase();if(l==="succeeded")return"online";if(l==="failed")return"failed";if(l==="running"||l==="queued")return"warn";return l}function nm(u){let l=Number(u?.durationMs);if(Number.isFinite(l)&&l>=0)return b0(l/1000);let f=Q0(u?.startedAt||u?.createdAt);if(f===null)return"--";let n=Q0(u?.finishedAt)??Date.now();return b0(Math.max(0,(n-f)/1000))}function UL(u){return{id:"unidesk-pgdata-baidu-daily",name:"PGDATA daily Baidu Netdisk backup",description:"Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.",enabled:!0,timeOfDay:"03:30",actionType:"pgdata_backup",providerId:u[0]?.providerId||"main-server",command:"echo",payloadJson:JSON.stringify({source:"scheduled-task",message:"hello from scheduler"},null,2),remoteBaseDir:"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:"server-data/unidesk-pg-data",timeoutMs:"3600000"}}function im({schedules:u,scheduleRuns:l,nodes:f,refresh:r,onRaw:n}){let[i,y]=bu(UL(f||[])),[t,_]=bu(!1),[c,A]=bu(""),[j,F]=bu(""),J=[...l||[]].sort((W,z)=>(Q0(z.updatedAt)??0)-(Q0(W.updatedAt)??0));function Q(W,z){y((Z)=>({...Z,[W]:z}))}function w(W){let z=W?.action||{};y({id:W?.id||"",name:W?.name||"",description:W?.description||"",enabled:W?.enabled!==!1,timeOfDay:W?.schedule?.timeOfDay||"03:30",actionType:z.type||"dispatch",providerId:z.providerId||f[0]?.providerId||"main-server",command:z.command||"echo",payloadJson:JSON.stringify(z.payload||{source:"scheduled-task"},null,2),remoteBaseDir:z.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:z.stagingSubdir||"server-data/unidesk-pg-data",timeoutMs:String(z.timeoutMs||3600000)}),F(`正在编辑 ${W?.id||""}`)}function L(){let W={id:i.id,name:i.name,description:i.description,enabled:i.enabled,concurrencyPolicy:"skip",schedule:{type:"daily",timeOfDay:i.timeOfDay,timezone:"Etc/UTC"}};if(i.actionType==="pgdata_backup")return{...W,action:{type:"pgdata_backup",volumeName:"unidesk_pgdata_10gb",remoteBaseDir:i.remoteBaseDir,stagingSubdir:i.stagingSubdir,timeoutMs:Number(i.timeoutMs)||3600000,cleanupLocal:!0}};return{...W,action:{type:"dispatch",providerId:i.providerId,command:i.command,payload:JSON.parse(i.payloadJson||"{}"),timeoutMs:Number(i.timeoutMs)||600000}}}async function U(W){W.preventDefault(),_(!0),A(""),F("");try{let z=L(),Z=encodeURIComponent(String(z.id));await Tu(`${gu.apiBaseUrl}/schedules/${Z}`,{method:"PUT",body:JSON.stringify(z)}),F("定时任务已保存"),await r()}catch(z){A(Ou(z,"保存定时任务失败"))}finally{_(!1)}}async function N(W){if(!W?.id)return;_(!0),A(""),F("");try{await Tu(`${gu.apiBaseUrl}/schedules/${encodeURIComponent(W.id)}`,{method:"DELETE"}),F(`已删除 ${W.id}`),await r()}catch(z){A(Ou(z,"删除定时任务失败"))}finally{_(!1)}}async function q(W){if(!W?.id)return;_(!0),A(""),F("");try{let z=await Tu(`${gu.apiBaseUrl}/schedules/${encodeURIComponent(W.id)}/run`,{method:"POST",body:"{}"});F(`已触发 ${W.id} / ${z?.run?.id||"run"}`),await r()}catch(z){A(Ou(z,"触发定时任务失败"))}finally{_(!1)}}return $("div",{className:"page-grid scheduled-task-page","data-testid":"scheduled-task-page"},$(du,{title:"定时任务",eyebrow:`${(u||[]).length} Schedules`},(u||[]).length===0?$(jl,{title:"暂无定时任务",text:"创建 daily / dispatch / PGDATA backup 任务后会在这里展示下一次执行时间和最近结果"}):$("div",{className:"schedule-card-grid"},(u||[]).map((W)=>$("article",{key:W.id,className:"schedule-card","data-testid":`schedule-row-${ef(W.id)}`},$("div",{className:"node-card-head"},$("strong",null,W.name||W.id),$(wl,{status:W.enabled?"online":"warn"},W.enabled?"enabled":"disabled")),$("code",null,W.id),$("dl",null,$("dt",null,"计划"),$("dd",null,lm(W.schedule)),$("dt",null,"动作"),$("dd",null,fm(W.action)),$("dt",null,"下次执行"),$("dd",null,qu(W.nextRunAt)),$("dt",null,"最近执行"),$("dd",null,W.lastRunAt?`${qu(W.lastRunAt)} / ${W.lastRunId||"--"}`:"--")),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>w(W)},"编辑"),$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>q(W),"data-testid":`schedule-run-${ef(W.id)}`},"手动触发"),$("button",{type:"button",className:"ghost-btn danger",disabled:t,onClick:()=>N(W)},"删除"),$(bl,{title:`Schedule ${W.id}`,data:W,onOpen:n})))))),$(du,{title:i.id?"配置定时任务":"新建定时任务",eyebrow:"CRUD"},$("form",{className:"dispatch-form schedule-form",onSubmit:U},$("label",null,"ID",$("input",{value:i.id,onChange:(W)=>Q("id",W.target.value)})),$("label",null,"名称",$("input",{value:i.name,onChange:(W)=>Q("name",W.target.value)})),$("label",null,"每日执行时间 UTC",$("input",{value:i.timeOfDay,placeholder:"03:30",onChange:(W)=>Q("timeOfDay",W.target.value)})),$("label",null,"启用",$("select",{value:i.enabled?"true":"false",onChange:(W)=>Q("enabled",W.target.value==="true")},$("option",{value:"true"},"enabled"),$("option",{value:"false"},"disabled"))),$("label",null,"动作类型",$("select",{value:i.actionType,onChange:(W)=>Q("actionType",W.target.value)},$("option",{value:"pgdata_backup"},"PGDATA 备份到百度网盘"),$("option",{value:"dispatch"},"Provider Dispatch"))),i.actionType==="pgdata_backup"?[$("label",{key:"remote"},"网盘根目录",$("input",{value:i.remoteBaseDir,onChange:(W)=>Q("remoteBaseDir",W.target.value)})),$("label",{key:"staging"},"本地 staging 子目录",$("input",{value:i.stagingSubdir,onChange:(W)=>Q("stagingSubdir",W.target.value)}))]:[$("label",{key:"provider"},"Provider",$("select",{value:i.providerId,onChange:(W)=>Q("providerId",W.target.value)},(f||[]).map((W)=>$("option",{key:W.providerId,value:W.providerId},`${W.name} / ${W.providerId}`)))),$("label",{key:"command"},"Command",$("select",{value:i.command,onChange:(W)=>Q("command",W.target.value)},$("option",{value:"echo"},"echo"),$("option",{value:"docker.ps"},"docker.ps"),$("option",{value:"host.ssh"},"host.ssh"),$("option",{value:"microservice.http"},"microservice.http"))),$("label",{key:"payload",className:"raw-editor-label"},"Payload JSON",$("textarea",{className:"raw-editor",value:i.payloadJson,onChange:(W)=>Q("payloadJson",W.target.value)}))],$("label",null,"超时 ms",$("input",{value:i.timeoutMs,onChange:(W)=>Q("timeoutMs",W.target.value)})),$("label",{className:"raw-editor-label"},"描述",$("textarea",{className:"raw-editor compact",value:i.description,onChange:(W)=>Q("description",W.target.value)})),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>y(UL(f||[]))},"重置"),$("button",{type:"submit",disabled:t||!i.id},t?"保存中":"保存任务")),j?$("p",{className:"muted paragraph"},j):null,$(il,{error:c,wide:!0}))),$(du,{title:"历史执行记录",eyebrow:`${J.length} Runs`},J.length===0?$(jl,{title:"暂无执行记录",text:"定时触发或手动触发后会生成 run history"}):$("div",{className:"table-wrap"},$("table",{className:"task-history-table schedule-run-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"触发"),$("th",null,"耗时"),$("th",null,"结果摘要"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,J.map((W)=>$("tr",{key:W.id,"data-testid":`schedule-run-row-${ef(W.id)}`},$("td",null,$(wl,{status:rm(W.status)},W.status)),$("td",null,$("strong",null,W.scheduleId),$("code",null,W.id),W.taskId?$("code",null,W.taskId):null),$("td",null,W.trigger||"--"),$("td",null,nm(W)),$("td",null,$(sn,{data:W.result||W.error,empty:"无结果"})),$("td",null,qu(W.updatedAt)),$("td",null,$(bl,{title:`Schedule Run ${W.id}`,data:W,onOpen:n})))))))))}function ym({data:u}){let l=u.overview||{};return $("div",{className:"page-grid topology-grid"},$(du,{title:"公开入口",eyebrow:"Public"},$("div",{className:"endpoint-list"},$("article",null,$("b",null,"Frontend"),$("span",null,gu.frontendPublicUrl||window.location.origin),$(wl,{status:"online"},"public")),$("article",null,$("b",null,"Provider Ingress"),$("span",null,gu.providerIngressPublicUrl||"ws://public/ws/provider"),$(wl,{status:"online"},"public")))),$(du,{title:"内部服务",eyebrow:"Docker Network Only"},$("div",{className:"endpoint-list"},$("article",null,$("b",null,"backend-core API"),$("span",null,"http://backend-core:8080"),$(wl,{status:"internal"},"internal")),$("article",null,$("b",null,"database"),$("span",null,"postgres://database:5432/unidesk"),$(wl,{status:"internal"},"internal")))),$(du,{title:"运行态",eyebrow:"Runtime"},$("div",{className:"metric-grid"},$(cl,{label:"DB Ready",value:l.dbReady?"YES":"NO",hint:"internal health"}),$(cl,{label:"Online Nodes",value:l.onlineNodeCount??0,hint:"provider-gateway self-link"}))))}function tm({session:u}){return $(du,{title:"认证策略",eyebrow:"Frontend Login"},$("div",{className:"policy-grid"},$("article",null,$("span",null,"默认账号"),$("strong",null,gu.authUsername||"admin")),$("article",null,$("span",null,"当前会话"),$("strong",null,u?.user?.username||"--")),$("article",null,$("span",null,"Session TTL"),$("strong",null,`${gu.sessionTtlSeconds||0}s`)),$("article",null,$("span",null,"API 访问"),$("strong",null,"同源 Cookie 保护"))),$("p",{className:"muted paragraph"},"浏览器只访问 frontend 同源接口;frontend 容器使用 Docker 内网代理 backend-core API。"))}function _m(){return $(du,{title:"安全边界",eyebrow:"Exposure Rule"},$("div",{className:"security-board"},$("article",{className:"allow"},$("b",null,"允许公网"),$("span",null,"frontend 登录入口"),$("span",null,"provider ingress WebSocket/health")),$("article",{className:"deny"},$("b",null,"禁止公网"),$("span",null,"backend-core REST API"),$("span",null,"PostgreSQL database")),$("article",null,$("b",null,"数据库卷"),$("span",null,"named volume unidesk_pgdata_10gb"),$("span",null,"CLI stop/start 不删除数据卷"))))}function $m({activeModule:u,activeTab:l,data:f,session:r,refresh:n,onRaw:i,onNavigate:y}){if(u==="ops"&&l==="status")return $(Ep,{data:f,onRaw:i,onNavigate:y});if(u==="ops"&&l==="performance")return $(Rp,{onRaw:i});if(u==="ops"&&l==="events")return $(Op,{events:f.events,onRaw:i});if(u==="ops"&&l==="logs")return $(Hp,{logs:f.logs,onRaw:i});if(u==="nodes"&&l==="list")return $(Bp,{nodes:f.nodes,onRaw:i});if(u==="nodes"&&l==="monitor")return $(Xp,{nodes:f.nodes,systemStatuses:f.systemStatuses,tasks:f.tasks,onRaw:i,refresh:n});if(u==="nodes"&&l==="docker")return $(Ip,{nodes:f.nodes,dockerStatuses:f.dockerStatuses,onRaw:i});if(u==="nodes"&&l==="gateway")return $(bp,{nodes:f.nodes,tasks:f.tasks,onRaw:i});if(u==="nodes"&&l==="labels")return $(Vp,{nodes:f.nodes});if(u==="nodes"&&l==="heartbeats")return $(Dp,{nodes:f.nodes});if(u==="tasks"&&l==="dispatch")return $(ap,{nodes:f.nodes,onDispatched:n,onRaw:i});if(u==="tasks"&&l==="scheduled")return $(im,{schedules:f.schedules,scheduleRuns:f.scheduleRuns,nodes:f.nodes,refresh:n,onRaw:i});if(u==="tasks"&&l==="pending")return $(dp,{tasks:f.pendingTasks,onRaw:i});if(u==="tasks"&&l==="history")return $(ep,{tasks:f.tasks,onRaw:i});if(u==="tasks"&&l==="results")return $(um,{tasks:f.tasks,onRaw:i});if(u==="apps"&&l==="catalog")return $(sp,{microservices:f.microservices,onRaw:i,onNavigate:y});if(u==="apps"&&l==="todo-note")return $(aw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="findjob")return $(hQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="pipeline")return $(xw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="met-nonlinear")return $(gQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="claudeqq")return $(qJ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="baidu-netdisk")return $(JJ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="filebrowser")return $(xQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="oa-event-flow")return $(lN,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="v3sctl")return $(fL,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl,onNavigate:y});if(u==="apps"&&l==="code-queue")return $(pQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl,initialTasksData:_p});if(u==="apps"&&l==="project-manager")return $(vw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="config"&&l==="topology")return $(ym,{data:f});if(u==="config"&&l==="auth")return $(tm,{session:r});if(u==="config"&&l==="security")return $(_m);return $(jl,{title:"未找到页面",text:"请选择左侧主模块和顶部子功能标签"})}function cm({session:u,onLogout:l}){let f=j2(Dr,window.location.pathname),[r,n]=bu(f.moduleId),[i,y]=bu({...st,[f.moduleId]:f.tabId}),[t,_]=bu({overview:null,nodes:[],systemStatuses:[],dockerStatuses:[],microservices:[],events:[],tasks:[],pendingTasks:[],schedules:[],scheduleRuns:[],logs:[]}),[c,A]=bu({ok:!1,text:"连接中"}),[j,F]=bu(null),[J,Q]=bu(new Date),[w,L]=bu(null),[U,N]=bu(!1),[q,W]=bu(!1),z=gn.default.useRef(!1),Z=Dr.moduleById[r]||Dr.modules[0],H=i[r]||st[r]||Z.tabs[0].id,E=Array.isArray(t.microservices)?t.microservices:[],D=E.length===0&&r==="apps"&&H==="code-queue"?[$p]:E,h=D===E?t:{...t,microservices:D},V=r==="apps"?D.find((o)=>String(o?.id||"")===(H==="v3sctl"?"v3sctl-adapter":H)):null,S=V?ZL(V):{},p=Z.tabs.find((o)=>o.id===H)?.label||H,O=V?[{key:"microservice",label:"用户服务",value:`${p} ${S.providerStatus==="online"?"在线":S.providerStatus||"未知"}`,tone:S.providerStatus==="online"?"ok":"warn",testId:"active-microservice-status"}]:[];async function m(){if(z.current)return;z.current=!0,W(!0);try{let o=[],g=(ju,zu)=>{o.push([ju,Tu(zu)])},x=r==="ops"&&H==="status",lu=x||r==="config"&&H==="topology",_u=x||r==="nodes"||r==="tasks"&&(H==="dispatch"||H==="scheduled"),$u=r==="apps"&&H!=="code-queue";if(lu)g("overview",`${gu.apiBaseUrl}/overview`);if(_u)g("nodes",`${gu.apiBaseUrl}/nodes`);if(r==="nodes"&&H==="monitor")g("systemStatuses",`${gu.apiBaseUrl}/nodes/system-status?limit=60`),g("tasks",`${gu.apiBaseUrl}/tasks?limit=120&summary=1`);else if(r==="nodes"&&H==="docker")g("dockerStatuses",`${gu.apiBaseUrl}/nodes/docker-status`);else if(r==="nodes"&&H==="gateway")g("tasks",`${gu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(r==="tasks"&&H==="scheduled")g("schedules",`${gu.apiBaseUrl}/schedules?limit=100`),g("scheduleRuns",`${gu.apiBaseUrl}/schedules/runs?limit=100`);else if(r==="tasks"&&H==="pending")g("pendingTasks",`${gu.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`);else if(r==="tasks"&&(H==="history"||H==="results"))g("tasks",`${gu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(x)g("tasks",`${gu.apiBaseUrl}/tasks?limit=8&lite=1`),g("pendingTasks",`${gu.apiBaseUrl}/tasks?status=pending&limit=20&lite=1`);if($u)g("microservices",`${gu.apiBaseUrl}/microservices`);if(r==="ops"&&H==="events")g("events",`${gu.apiBaseUrl}/events?limit=100`);if(r==="ops"&&H==="logs")g("logs","/logs?limit=100");await Promise.all(o.map(async([ju,zu])=>{let Wu=await zu,P={};if(ju==="overview")P.overview=Wu;if(ju==="nodes")P.nodes=Wu.nodes||[];if(ju==="systemStatuses")P.systemStatuses=Wu.systemStatuses||[];if(ju==="dockerStatuses")P.dockerStatuses=Wu.dockerStatuses||[];if(ju==="microservices")P.microservices=Wu.microservices||[];if(ju==="events")P.events=Wu.events||[];if(ju==="tasks")P.tasks=Wu.tasks||[];if(ju==="pendingTasks")P.pendingTasks=Wu.tasks||[];if(ju==="schedules")P.schedules=Wu.schedules||[];if(ju==="scheduleRuns")P.scheduleRuns=Wu.runs||[];if(ju==="logs")P.logs=Wu.logs||[];_((e)=>({...e,...P}))})),A({ok:!0,text:"核心在线"}),F(new Date)}catch(o){if(A({ok:!1,text:Ou(o,"连接失败")}),o.status===401)l(!1)}finally{z.current=!1,W(!1)}}J0(()=>{let o=()=>{if(!iL())return;m()};o();let g=setInterval(o,cp(r,H)),x=()=>{if(iL())o()};return document.addEventListener("visibilitychange",x),()=>{clearInterval(g),document.removeEventListener("visibilitychange",x)}},[r,H]),J0(()=>{let o=setInterval(()=>Q(new Date),1000);return()=>clearInterval(o)},[]),J0(()=>{let o=dQ(Dr,window.location.pathname);if(o&&window.location.pathname!==o)window.history.replaceState(null,"",o)},[]),J0(()=>{let o=()=>{let g=j2(Dr,window.location.pathname);n(g.moduleId),y((x)=>({...x,[g.moduleId]:g.tabId})),L(null)};return window.addEventListener("popstate",o),()=>window.removeEventListener("popstate",o)},[]),J0(()=>{window.scrollTo({top:0,left:0,behavior:"auto"})},[r,H]);function X(o,g,x="push"){let lu=Dr.moduleById[o]?o:Dr.fallbackTarget.moduleId,_u=Dr.moduleById[lu]?.tabs.some((ju)=>ju.id===g)?g:st[lu]||Dr.moduleById[lu]?.tabs[0]?.id||Dr.fallbackTarget.tabId;n(lu),y((ju)=>({...ju,[lu]:_u}));let $u=yc(Dr,lu,_u);if(window.location.pathname!==$u){let ju=x==="replace"?"replaceState":"pushState";window.history[ju](null,"",$u)}}function v(o,g){L({title:o,data:g})}let[T,Y]=bu(!1),{unreadCount:k,notifications:I}=Xf(),b=I.length>0?I[I.length-1]:null;return $("div",{className:`shell ${U?"rail-collapsed":""}`,"data-testid":"app-shell"},$(zp,{activeModule:r,activeTabs:i,onNavigate:X,collapsed:U,onToggle:()=>N((o)=>!o)}),$("main",{className:"workspace"},$(Kp,{connection:c,lastRefresh:j,onRefresh:m,onLogout:()=>l(!0),session:u,clock:J,activeStatusItems:O,onNotificationToggle:()=>Y((o)=>!o),unreadCount:k}),$(Tp,{module:Z,activeTab:H,onNavigate:X}),$(n7.Provider,{value:q},$($m,{activeModule:r,activeTab:H,data:h,session:u,refresh:m,onRaw:v,onNavigate:X}))),$(wp,{raw:w,onClose:()=>L(null)}),b&&$(nL,{key:b.id,notification:b}),T&&$(rL,{onClose:()=>Y(!1)}))}function Am(){let[u,l]=bu(!0),[f,r]=bu(null);async function n(){l(!0);try{let y=await Tu("/api/session");r(y.authenticated?y:null)}catch{r(null)}finally{l(!1)}}async function i(y){if(y)try{await Tu("/logout",{method:"POST"})}catch{}r(null)}if(J0(()=>{n()},[]),u)return $("main",{className:"loading-screen"},$("div",{className:"brand-mark"},"UD"),$("span",null,"加载会话"));if(!f)return $(Lp,{onLogin:r});return $(UJ,null,$(cm,{session:f,onLogout:i}))}var XL=document.getElementById("root");if(XL===null)throw Error("root element not found");JL.createRoot(XL).render($(Am));})(); + `,width:w,height:L}}function K6(u,l){let f=URL.createObjectURL(u),r=document.createElement("a");r.href=f,r.download=l,r.click(),setTimeout(()=>URL.revokeObjectURL(f),1000)}async function Xw(u,l){let f=Dw(l,"pipeline"),{svg:r,width:n,height:i}=uY(u,l),y=new Blob([r],{type:"image/svg+xml;charset=utf-8"}),t=URL.createObjectURL(y);try{let _=new Image;await new Promise((F,J)=>{_.onload=()=>F(),_.onerror=()=>J(Error("svg image load failed")),_.src=t});let c=document.createElement("canvas");c.width=n,c.height=i;let A=c.getContext("2d");if(!A)throw Error("canvas unavailable");A.drawImage(_,0,0);let j=await new Promise((F)=>c.toBlob(F,"image/png"));if(!j)throw Error("png export failed");K6(j,`${f}.png`)}catch{K6(y,`${f}.svg`)}finally{URL.revokeObjectURL(t)}}async function iY(u){let l=Dw(String(u?.title||"pipeline-gantt"),"pipeline-gantt"),{svg:f,width:r,height:n}=nY(u),i=new Blob([f],{type:"image/svg+xml;charset=utf-8"}),y=URL.createObjectURL(i);try{let t=new Image;await new Promise((j,F)=>{t.onload=()=>j(),t.onerror=()=>F(Error("gantt svg image load failed")),t.src=y});let _=document.createElement("canvas");_.width=r,_.height=n;let c=_.getContext("2d");if(!c)throw Error("canvas unavailable");c.drawImage(t,0,0);let A=await new Promise((j)=>_.toBlob(j,"image/png"));if(!A)throw Error("gantt png export failed");K6(A,`${l}.png`)}catch{K6(i,`${l}.svg`)}finally{URL.revokeObjectURL(y)}}async function yY(u){for(let l of u){if(l.flow.nodes.length===0)continue;await Xw(l.flow,l.title),await new Promise((f)=>setTimeout(f,750))}}function Qw(u,l){return u.find((f)=>String(f?.pipelineId||"")===l)||null}function Nw(u){return Iu(u?.startedAt)??Iu(u?.artifact?.startedAt)??Iu(u?.request?.createdAt)??Iu(u?.updatedAt)??0}function tY(u,l){return u.filter((f)=>String(f?.pipelineId||"")===l).slice().sort((f,r)=>Nw(f)-Nw(r)||String(f?.runId||"").localeCompare(String(r?.runId||"")))}function X9(u,l){let f=String(l?.runId||""),r=u.findIndex((y)=>String(y?.runId||"")===f),n=r>=0?r+1:u.length,i=String(l?.status||"--");return`Epoch ${n} / ${f||"--"} / ${i}`}function Vr(u){return String(u?.procedureRunId||u?.runId||"")}function E6(u,l){let f=String(u?.nodeId||u?.request?.nodeId||"");if(f)return f;let r=Vr(u),n=`${l}__`;if(r.startsWith(n))return r.slice(n.length).replace(/__\d+$/u,"");return""}function F6(u,l){let f=Xu(u?.artifact)?u.artifact:{},r=Xu(u?.request)?u.request:{};return C_(u?.startedAt,f.startedAt,r.createdAt,r.startedAt,u?.createdAt,u?.updatedAt,l?.startedAt,l?.request?.createdAt)}function U6(u,l){let f=String(u?.status?.status||u?.artifact?.status||u?.status||"").toLowerCase(),r=Xu(u?.artifact)?u.artifact:{},n=m9(f);return C_(u?.finishedAt,r.finishedAt,u?.completedAt,n?u?.updatedAt:void 0,n?r.updatedAt:void 0,n?l?.updatedAt:void 0)}function Sw(u,l,f=Date.now()){let r=String(u?.runId||""),n=new Set(l.map((i)=>String(i?.id||"")).filter(Boolean));return Du(u?.procedureRuns).flatMap((i)=>{let y=E6(i,r);if(!y)return[];let t=String(i?.status?.status||i?.artifact?.status||i?.status||"unknown").toLowerCase(),_=F6(i,u),c=Iu(_);if(c===null)return[];let A=U6(i,u),j=Iu(A)??(m9(t)?Iu(i?.updatedAt)??c+1000:f),F=Math.max(c+1000,j);return[{nodeId:y,knownNode:n.has(y),procedureRunId:Vr(i),status:t,startMs:c,endMs:F,startedAt:p_(c),finishedAt:p_(F),durationMs:F-c,runId:r,raw:i}]}).sort((i,y)=>i.startMs-y.startMs||i.endMs-y.endMs||i.nodeId.localeCompare(y.nodeId))}function _Y(u,l,f=[]){let r=l.map((A)=>Number(A.startMs)).filter(Number.isFinite),n=l.map((A)=>Number(A.endMs)).filter(Number.isFinite);for(let A of f){let j=Vl(A?.eventMs??A?.ms);if(j!==null)r.push(j),n.push(j)}let i=Iu(u?.startedAt)??Iu(u?.artifact?.startedAt)??Iu(u?.request?.createdAt),y=Iu(u?.finishedAt)??Iu(u?.artifact?.finishedAt)??Iu(u?.updatedAt);if(i!==null)r.push(i);if(y!==null)n.push(y);let t=Date.now(),_=r.length>0?Math.min(...r):t-60000,c=Math.max(_+60000,n.length>0?Math.max(...n):t);return{startMs:_,endMs:c,durationMs:c-_}}var J6=12,Yw=20,S9=100,$Y=!1;function vn(u){let l=Number(u);if(!Number.isFinite(l))return 0;return Math.max(0,Math.min(100,Math.round(l*100)/100))}function cY(u){let l=Math.max(J6,Number(u||J6)),f=Math.log(l/J6)/Math.log(Yw);return vn(f*100)}var P_=cY(S9);function h9(u){let l=vn(u)/100,f=J6*Math.pow(Yw,l),r=l<0.24?"全局":l<0.64?"均衡":"细节";return{value:vn(l*100),pxPerMinute:f,label:r}}function E9(u){let l=Math.round(Number(u));return Math.abs(l-S9)<=1?S9:l}function AY(u,l=P_){let f=Math.max(1,Number(u.durationMs||0)/60000),r=h9(l);return Math.round(Math.max(360,Math.min(7200,f*Number(r.pxPerMinute||48))))}function jY(u,l=7){let f=Math.max(1,Number(u.endMs||0)-Number(u.startMs||0));return Array.from({length:l},(r,n)=>{let i=l===1?0:n/(l-1);return{ms:Number(u.startMs)+f*i,percent:i*100}})}function FY(u,l){let f=Math.max(1,Number(l.endMs)-Number(l.startMs));return Math.max(0,Math.min(100,(u-Number(l.startMs))/f*100))}function Vl(u){let l=Number(u);return Number.isFinite(l)?l:null}function b9(u){return Lw(u?.status)&&!m9(u?.status)}function pw(u,l,f,r){let n=Math.max(1,f-l),i=Math.max(0,Math.min(1,(u-l)/n));return Number((i*r).toFixed(3))}function qw(u,l){if(!l)return null;let f=Vl(l?.startMs),r=Vl(l?.endMs),n=Vl(l?.chartHeight);if(f===null||r===null||n===null)return null;return pw(u,f,r,n)}function mw(u,l){let f=Vl(u?.rawStartMs??u?.startMs)??Vl(u?.startMs)??l,r=Vl(u?.endMs)??f+1000;if(!b9(u))return Math.max(f+1000,r);return Math.max(f+1000,r,l)}function UY(u,l,f,r){let n=Vl(u?.startMs)??r-60000,i=Vl(u?.endMs)??r,y=f.reduce((Q,w)=>Math.max(Q,mw(w,r)),i),t=Math.max(n+60000,i,y),_=Math.max(1,t-n),c={startMs:n,endMs:t,durationMs:_},A=AY(c,l),j=h9(l),F=Math.max(5,Math.min(18,Math.round(A/150))),J=jY(c,F).map((Q)=>{let w=Number(Q.ms),L=pw(w,n,t,A);return{...Q,y:L,timestamp:p_(w),offsetMs:w-n}});return{source:"frontend-y",startMs:n,endMs:t,durationMs:_,chartHeight:A,scale:vn(l),normalizedScale:Number((vn(l)/100).toFixed(3)),pxPerMinute:Number(Number(j.pxPerMinute||0).toFixed(3)),ticks:J}}function JY(u,l,f){if(!b9(u))return u;let r=Vl(u?.rawStartMs??u?.startMs)??Vl(u?.startMs)??f,n=mw(u,f),i=qw(r,l),y=qw(n,l),t=Vl(i??u?.y1??u?.startY)??0,_=Vl(y??u?.y2??u?.endY)??t+10,c=Math.max(24,_-t);return{...u,live:!0,startMs:r,endMs:n,durationMs:Math.max(1000,n-r),finishedAt:p_(n),y1:t,y2:_,startY:t,endY:_,height:c}}function v9(u,l,f){return FY(u,l)/100*f}function Qy(u){return Boolean(u&&String(u?.source||"")!=="frontend-y")}function Pw(u,l,f,r,n){if(Qy(r))for(let y of n){let t=Vl(u?.[y]);if(t!==null)return t}let i=Vl(u?.ms??u?.eventMs??u?.startMs);return v9(i??Number(l.startMs),l,f)}function G6(u,l,f,r){return Pw(u,l,f,r,["y1","startY"])}function Y9(u,l,f,r){if(Qy(r)){let i=Vl(u?.y2??u?.endY);if(i!==null)return i}let n=Vl(u?.endMs)??Number(l.endMs);return v9(n,l,f)}function Cw(u,l,f,r){if(Qy(r)){let i=Vl(u?.height);if(i!==null)return Math.max(1,i)}let n=u?.live?24:10;return Math.max(n,Y9(u,l,f,r)-G6(u,l,f,r))}function Jr(u,l,f,r){return Pw(u,l,f,r,["y","timeAxisY"])}function Mw(u,l,f,r){if(Qy(r)||String(r?.source||"")==="frontend-y"){let y=Vl(u?.y);if(y!==null)return y}let n=Vl(u?.percent);if(n!==null)return n/100*f;let i=Vl(u?.ms)??Number(l.startMs);return v9(i,l,f)}function QY(u){let l=String(u?.promptEvent||u?.raw?.promptEvent||u?.event||"").toLowerCase();if(!["node-long-running-observation","node-finished"].includes(l))return"";let f=String(u?.sourceNodeId||u?.raw?.sourceNodeId||u?.raw?.detail?.nodeId||""),r=String(u?.nodeId||u?.targetNodeId||"");return f&&f!==r?f:""}function NY(u,l){let f=new Set(l.map((n)=>[String(n.sourceNodeId||""),String(n.targetNodeId||""),String(n.targetMarkerId||""),String(n.action||"")].join(":"))),r=[...l];for(let n of u){let i=QY(n),y=String(n?.nodeId||""),t=String(n?.id||"");if(!i||!y||!t)continue;let _=[i,y,t,"observe"].join(":");if(f.has(_))continue;f.add(_),r.push({id:`observation-arrow:${t}:${i}:${y}`,commandId:String(n?.commandId||n?.eventId||t),sourceNodeId:i,targetNodeId:y,sourceMarkerId:"",targetMarkerId:t,sourceKind:"monitor",action:"observe",status:"observation"})}return{markers:u,arrows:r}}function qY(u){let l=gr(u),f=String(u?.promptEvent||"");if(l==="initial-prompt-delivered")return"initial";if(f==="node-finished"||f==="node-long-running-observation"||f.startsWith("monitor-"))return"monitor";if(l==="monitor-prompt-delivered"||String(u?.sourceKind||"").toLowerCase()==="monitor")return"monitor";return"append"}function WY(u){return Du(u?.tags||u?.raw?.tags).map((l)=>String(l||"")).filter(Boolean)}function wY(u){let l=gr(u),f=String(u?.promptEvent||"");if(l==="initial-prompt-delivered")return"初始 prompt";if(f==="node-long-running-observation")return"长任务观察";if(f==="node-finished")return WY(u).includes("monitor.audit")?"节点完成 / OA 审核":"节点完成";if(f==="monitor-interval")return"Monitor observation";if(f==="monitor-start")return"Monitor start";if(f==="monitor-stop")return"Monitor stop";if(l==="monitor-prompt-delivered")return"Monitor prompt";if(l==="append-prompt-queued")return"追加 prompt 已排队";return"追加 prompt"}function Ww(u){let l=gr(u);if(l==="control-command-applied")return 3;if(l==="control-command-ignored")return 2;if(l==="control-command-queued")return 1;return 0}function LY(u,l){let f=String(u?.commandId||"");if(f)return`command:${f}`;return["control-event",Fy(u)||C_(u?.createdAt,u?.timestamp)||`index-${l}`,String(u?.sourceKind||""),String(u?.sourceNodeId||""),String(u?.targetNodeId||""),xi(u)].join(":")}function KY(u){return V9([u?.targetNodeId,...Du(u?.resetNodeIds)])}function GY(u,l){let f=S_(u),r=gr(u),n=String(u?.targetNodeId||""),i=Boolean(n)&&l!==n;if(r==="control-command-applied")return i?`${f} 波及`:`${f} 生效`;if(r==="control-command-ignored")return`${f} 忽略`;if(r==="control-command-queued")return`${f} 已发起`;return i?`${f} 波及`:f}function zY(u){if(gr(u)==="control-command-ignored")return"ignored";let f=xi(u);if(f==="restart"||f==="redo")return"restart";if(f==="modify")return"modify";if(f==="approve")return"approve";if(f==="guide")return"guide";return"pending"}function TY(u){let l=String(u?.sourceKind||"").toLowerCase();if(l==="monitor")return"monitor";if(l==="webui")return"webui";if(l==="cli")return"cli";return"system"}function EY(u,l,f,r){let n=u.filter((c)=>String(c.nodeId||"")===l).sort((c,A)=>Number(c.startMs)-Number(A.startMs)),i=n.find((c)=>f>=Number(c.startMs)-1000&&f<=Number(c.endMs)+1000);if(i)return{ms:f,onInterval:!0,snapReason:"inside-interval",procedureRunId:String(i.procedureRunId||"")};let y=xi(r),t=n.slice().reverse().find((c)=>Number(c.endMs)<=f+1000);if(t&&y==="approve")return{ms:Number(t.endMs),onInterval:!0,snapReason:"previous-interval-end",procedureRunId:String(t.procedureRunId||"")};let _=n.find((c)=>Number(c.startMs)>=f-1000);if(_&&["guide","modify","restart","redo"].includes(y))return{ms:Number(_.startMs),onInterval:!0,snapReason:"next-interval-start",procedureRunId:String(_.procedureRunId||"")};return{ms:f,onInterval:!1,snapReason:"event-time",procedureRunId:String(r?.procedureRunId||"")}}function Rw(u,l,f,r){let n=Math.hypot(f-u,r-l),i=n>lw?lw:0,y=i>0?f-(f-u)/n*i:f,t=i>0?r-(r-l)/n*i:r,_=y-u,c=Math.max(16,Math.min(42,Math.abs(_)*0.45+12)),A=_===0?1:Math.sign(_);return`M ${u},${l} C ${u+A*c},${l} ${y-A*c},${t} ${y},${t}`}function ZY(u,l){let f=String(u?.runId||l?.runId||""),r=Sw({...Xu(l)?l:{},...Xu(u)?u:{},runId:f,procedureRuns:Du(u?.procedureRuns).length>0?u.procedureRuns:l?.procedureRuns},[]),n=[],i=[],y=[],t=new Set,_=new Map,c=(F,J)=>{if(!F.nodeId||!Number.isFinite(Number(F.ms)))return;if(t.has(F.id))return;t.add(F.id),J.push(F)};for(let F of Du(u?.procedureRuns)){let J=E6(F,f),Q=Vr(F);if(!J)continue;for(let w of Du(F?.attempts)){let L=T6(w);for(let U of B9(w?.controlEventRecords)){let N=gr(U);if(!["initial-prompt-delivered","append-prompt-delivered","monitor-prompt-delivered"].includes(N))continue;let q=Fy(U),W=Iu(q);if(W===null)continue;let z=String(U?.eventId||"");c({id:`prompt:${z||`${Q}:${L}:${N}:${W}`}`,runId:f,nodeId:J,procedureRunId:Q,attempt:L,kind:"prompt",tone:qY(U),status:"delivered",label:wY(U),ms:W,timestampIso:q,sourceKind:String(U?.sourceKind||""),sourceNodeId:String(U?.sourceNodeId||""),targetNodeId:J,action:"",eventId:z,commandId:String(U?.commandId||""),raw:U},n)}}}let A=new Map;B9(u?.controlEvents).forEach((F,J)=>{let Q=LY(F,J),w=A.get(Q)||{key:Q,events:[]};w.events.push(F),A.set(Q,w)});for(let F of A.values()){let J=Du(F.events).slice().sort((V,S)=>Ww(S)-Ww(V)),Q=Du(F.events).find((V)=>gr(V)==="control-command-queued")||null,w=J[0]||Q;if(!Q&&!w)continue;let L=String(Q?.sourceNodeId||w?.sourceNodeId||""),U=String(Q?.sourceKind||w?.sourceKind||""),N=Fy(Q)||Fy(w)||C_(Q?.createdAt,w?.createdAt),q=Iu(N),W=String(w?.commandId||Q?.commandId||F.key),z=(gr(w)||"control-command-queued").replace(/^control-command-/u,""),Z="";if(L&&q!==null)Z=`control-source:${W}:${L}`,_.set(W,Z),c({id:Z,runId:f,nodeId:L,procedureRunId:String(Q?.procedureRunId||w?.procedureRunId||""),attempt:"",kind:"control-source",tone:TY(Q||w),status:z,label:`${S_(Q||w)} 发起`,ms:q,timestampIso:N,action:xi(Q||w),sourceKind:U,sourceNodeId:L,targetNodeId:String(w?.targetNodeId||Q?.targetNodeId||""),commandId:W,raw:Q||w},i);let H=w||Q,E=Fy(H)||N,D=Iu(E);if(D===null)continue;let h=KY(H);for(let V of h){let S=EY(r,V,D,H),p=`control-target:${W}:${V}`;if(c({id:p,runId:f,nodeId:V,procedureRunId:S.procedureRunId,attempt:"",kind:"control-target",tone:zY(H),status:z,label:GY(H,V),ms:S.ms,eventMs:D,onInterval:S.onInterval,snapReason:S.snapReason,snapped:Number(S.ms)!==D,timestampIso:E,renderedTimestampIso:p_(Number(S.ms)),action:xi(H),sourceKind:U,sourceNodeId:L,targetNodeId:V,commandId:W,raw:H},i),Z&&L&&L!==V)y.push({id:`control-arrow:${W}:${L}:${V}`,commandId:W,sourceNodeId:L,targetNodeId:V,sourceMarkerId:Z,targetMarkerId:p,sourceKind:U,action:xi(H),status:z})}}let j=[...n,...i].sort((F,J)=>Number(F.ms)-Number(J.ms)||String(F.nodeId).localeCompare(String(J.nodeId))||String(F.id).localeCompare(String(J.id)));return{...NY(j,y),sourceMarkerByCommand:_}}function OY({details:u,selectedNodeId:l,selectedNodeRuntime:f,control:r,onRaw:n}){if(!u)return G("span",{className:"muted"},"点击“抓取过程”读取 node 运行材料;主界面只显示结构化摘要,完整内容需点开原始 JSON。");let i=Du(u.procedureRuns),y=i.at(-1)||{},t=Du(y.attempts),_=t.at(-1)||{},c=Du(y.workerLogTail),A=Du(_.controlEventsTail),j=Du(_.controlPromptsTail),F=Du(_.monitorPromptsTail),J=K9(A),Q=K9(j),w=K9(F),L=_.opencodeMessages||{};return G("div",{className:"pipeline-evidence-list compact"},G(Hr,{title:"Node runtime",subtitle:l||"--",facts:[`status ${f?.status||"pending"}`,`attempts ${f?.attempts??t.length}`,`procedure ${f?.currentProcedureRunId||Vr(y)||"--"}`,r.fetchedAt?`fetched ${tl(r.fetchedAt)}`:"not fetched"],data:u.node||u,onRaw:n,testId:"raw-pipeline-node-runtime"}),G(Hr,{title:"Procedure runs",subtitle:`${i.length} groups`,facts:[`latest ${y.status?.status||y.status||"--"}`,`steps ${Du(y.recentSteps).length}`,`duration ${Br(Iu(y.finishedAt)&&Iu(y.startedAt)?Number(Iu(y.finishedAt))-Number(Iu(y.startedAt)):y.durationMs)}`],data:i,onRaw:n,testId:"raw-pipeline-node-procedures"}),G(Hr,{title:"OpenCode messages",subtitle:String(L.exists?"available":"not indexed"),facts:[`messages ${N6(L.messageCount)}`,`size ${N6(L.size)}`,`updated ${qu(L.updatedAt)}`],data:L,onRaw:n,testId:"raw-pipeline-node-messages"}),G(Hr,{title:"Control prompts",subtitle:"manual / monitor append queues",facts:[`manual tail ${Q.total}`,`monitor tail ${w.total}`,`last ${qu(p9(Q.lastAt,w.lastAt))}`],data:{controlPromptsTail:j,monitorPromptsTail:F},onRaw:n,testId:"raw-pipeline-node-prompts"}),G(Hr,{title:"Control events",subtitle:J.eventKinds.length>0?J.eventKinds.join(", "):"event tail",facts:[`tail ${J.total}`,`parsed ${J.parsed}`,`last ${qu(J.lastAt)}`],data:A,onRaw:n,testId:"raw-pipeline-node-events"}),G(Hr,{title:"Worker log",subtitle:"tail is hidden on main canvas",facts:[`tail ${c.length} lines`,"raw only via button",`procedure ${Vr(y)||"--"}`],data:c,onRaw:n,testId:"raw-pipeline-node-worker-log"}))}function HY({activeRun:u,onRaw:l}){if(!u)return G(Qr,{title:"暂无运行材料",text:"没有 Pipeline epoch 时不会展示运行材料索引。"});let f=Du(u.nodes),r=Du(u.procedureRuns),n=Du(u.submissions),i=Du(u.workerLogTail),y=nw(f),t=nw(r),_=r.filter((A)=>String(A?.status||"").toLowerCase()==="failed"),c=p9(...r.flatMap((A)=>[A.updatedAt,A.finishedAt,A.startedAt]));return G("div",{className:"pipeline-evidence-list"},G(Hr,{title:"Epoch overview",subtitle:u.runId||"--",facts:[`pipeline ${u.pipelineId||"--"}`,`status ${u.status||"--"}`,`started ${qu(u.startedAt)}`,`updated ${qu(u.updatedAt)}`],data:u,onRaw:l,testId:"raw-pipeline-run"}),G(Hr,{title:"Node states",subtitle:`${f.length} nodes`,facts:[`running ${y.running||0}`,`succeeded ${y.succeeded||0}`,`failed ${y.failed||0}`,`pending ${y.pending||0}`],data:f,onRaw:l,testId:"raw-pipeline-run-nodes"}),G(Hr,{title:"Procedure run index",subtitle:`${r.length} procedure records`,facts:[`succeeded ${t.succeeded||0}`,`failed ${t.failed||0}`,`latest ${qu(c)}`,`errors ${_.length}`],data:r,onRaw:l,testId:"raw-pipeline-run-procedures"}),G(Hr,{title:"OA submissions",subtitle:`${n.length} submission files`,facts:[`records ${n.length}`,`task ${N6(u.task)}`,"raw grouped by run"],data:n,onRaw:l,testId:"raw-pipeline-run-submissions"}),G(Hr,{title:"Worker log tail",subtitle:"hidden from main interface",facts:[`tail ${i.length} lines`,"display raw only after click",`updated ${qu(u.updatedAt)}`],data:i,onRaw:l,testId:"raw-pipeline-run-worker-log"}))}function BY({diagnostics:u,onRaw:l}){let f=Du(u?.runs).filter(Xu),r=Du(u?.forbiddenResiduals),n=Xu(u?.guarantees)?u.guarantees:{},i=u?.hasNeutralNodeFinishedEvidence===!0&&u?.hasNoAuditPolicyEvidence===!0&&u?.hasAuditPolicyEvidence===!0,y=u?.ok===!0&&i&&r.length===0,t=f[0]||null,_=[{label:"中性完成事实",ok:n.neutralNodeFinished===!0,hint:"node-finished 不携带流程策略"},{label:"Config 策略判定",ok:n.auditPolicyFromConfig===!0,hint:"OA backend 读取当前 epoch 配置"},{label:"控制命令来自 OA",ok:n.runnerConsumesControlCommandsFromOaEvents===!0,hint:"runner 只消费 OA control.command"},{label:"无独立审核事件",ok:n.noIndependentAuditRequestEvent===!0,hint:"审核由 node-finished + policy 派生"},{label:"无批次门禁",ok:n.noBatchFinishedControlGate===!0,hint:"下游启动由每个 node 完成驱动"}];return G("div",{className:"pipeline-oa-panel","data-testid":"pipeline-oa-event-flow-panel"},G("div",{className:"metric-grid compact"},G(pf,{label:"OA Flow",value:y?"100%":"--",hint:String(u?.mode||"waiting diagnostics"),tone:y?"ok":"warn"}),G(pf,{label:"禁止残留",value:r.length,hint:r.length===0?"source scan clean":"needs cleanup",tone:r.length===0?"ok":"warn"}),G(pf,{label:"No-audit",value:u?.hasNoAuditPolicyEvidence?"OK":"--",hint:"OA 下游策略证据",tone:u?.hasNoAuditPolicyEvidence?"ok":"warn"}),G(pf,{label:"Monitor 审核",value:u?.hasAuditPolicyEvidence?"OK":"--",hint:"OA 控制事件闭环",tone:u?.hasAuditPolicyEvidence?"ok":"warn"})),G("div",{className:"pipeline-oa-guarantees"},_.map((c)=>G("article",{key:c.label,className:`pipeline-oa-guarantee ${c.ok?"ok":"warn"}`},G(bn,{status:c.ok?"online":"warn"},c.ok?"OK":"MISS"),G("div",null,G("strong",null,c.label),G("span",null,c.hint))))),G("div",{className:"pipeline-evidence-list compact"},f.slice(0,6).map((c)=>G(Hr,{key:c.runId,title:String(c.runId||"--"),subtitle:[Number(c.monitorAuditNodeFinishedCount||0)>0?"monitor audit":"",Number(c.noAuditPolicyCount||0)>0?"no-audit policy":""].filter(Boolean).join(" / ")||"event evidence",facts:[`events ${c.eventCount||0}`,`node-finished ${c.nodeFinishedCount||0}`,`policy-in-detail ${c.nodeFinishedWithPolicyCount||0}`,`queued ${c.controlQueuedCount||0}`,`applied ${c.controlAppliedCount||0}`],data:c,onRaw:l,testId:`raw-pipeline-oa-run-${String(c.runId||"run").replace(/[^a-zA-Z0-9_.-]+/g,"-")}`}))),t?G("p",{className:"muted paragraph"},`最新证据 ${t.runId}: ${t.nodeFinishedCount||0} 个 node-finished,${t.controlAppliedCount||0} 个控制结果。`):G(Qr,{title:"暂无 OA 事件流证据",text:"等待 Pipeline backend 暴露 diagnostics。"}),u?G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline OA Event Flow Diagnostics",data:u,onOpen:l,testId:"raw-pipeline-oa-event-flow"})):null)}function VY({quota:u,onRaw:l}){let f=Xu(u?.summary)?u.summary:{},r=Xu(u?.target)?u.target:{},n=Xu(u?.cache)?u.cache:{},i=u?.ok===!0,y=String(u?.modelId||f.modelName||r.modelName||"MiniMax-M2.7"),t=f.totalCount??r.currentIntervalTotalCount,_=f.usageCount??r.currentIntervalUsageCount,c=f.remainingCount??r.currentIntervalRemainingCount,A=f.remainingRatio??(Number.isFinite(Number(t))&&Number(t)>0&&Number.isFinite(Number(c))?Number(c)/Number(t):void 0),j=f.usageRatio??(Number.isFinite(Number(t))&&Number(t)>0&&Number.isFinite(Number(_))?Number(_)/Number(t):void 0),F=f.resetAt||r.endAt,J=f.remainsMs??r.remainsMs,Q=Number(c),w=!i||Number.isFinite(Q)&&Q<=0?"warn":"ok",L=[i?`endpoint ${u?.endpoint||"--"}`:"quota unavailable",`fetched ${Q6(u?.fetchedAt)}`,n.hit?`cache ${Br(n.ageMs)}`:"live quota"];return G("div",{className:"pipeline-minimax-quota-panel","data-testid":"pipeline-minimax-quota-panel"},G("div",{className:"metric-grid compact"},G(pf,{label:"MiniMax",value:i?y:"--",hint:u?.modelComponent||u?.error||"model/minimax-m27",tone:w}),G(pf,{label:"当前窗口",value:`${L9(_)}/${L9(t)}`,hint:`已用 ${rw(j)}`,tone:w}),G(pf,{label:"剩余额度",value:L9(c),hint:`剩余 ${rw(A)}`,tone:w}),G(pf,{label:"重置时间",value:Q6(F),hint:J!==void 0?`约 ${Br(J)}`:qu(F),tone:w})),G(P9,{items:L}),i?G("p",{className:"muted paragraph"},`MiniMax 限额来自 D601 Pipeline 后端实时查询;当前模型匹配 ${f.modelName||r.modelName||y}。`):G(il,{error:u?.error||"MiniMax 限额查询失败"}),u?G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline MiniMax Quota",data:u,onOpen:l,testId:"raw-pipeline-minimax-quota"})):null)}function DY({epochs:u,activeRun:l,activePipeline:f,pipelineNodes:r,pipelineEdges:n,runDetails:i,nodeDetails:y,nodeDetailsState:t,ganttScale:_=P_,onGanttScaleChange:c,onRunChange:A,onIntervalSelect:j,onMarkerSelect:F,selection:J,detailOpen:Q,onDetailOpenChange:w,onRaw:L}){let[U,N]=af($Y),[q,W]=af({startY:0,endY:0,startMs:0,endMs:0}),[z,Z]=af(Date.now()),H=xn(null),E=String(l?.runId||""),D=Boolean(Q),h=(Au)=>{if(typeof w==="function")w(Au)},V=vn(_??P_),S=String(i?.runId||"")===E?i?.details:null,p=S?{...Xu(l)?l:{},...Xu(S)?S:{},runId:E,procedureRuns:Du(S?.procedureRuns).length>0?S.procedureRuns:l?.procedureRuns}:l,O=Sw(p,r,z),m=S?ZY(S,p):{markers:[],arrows:[]},X=Du(m.markers),v=_Y(p,O,X),T=UY(v,V,O,z),Y=String(T.source||"frontend-y"),k=O.map((Au)=>JY(Au,T,z)),I={startMs:Number(T.startMs),endMs:Number(T.endMs),durationMs:Math.max(1,Number(T.durationMs??Number(T.endMs)-Number(T.startMs)))},b=h9(V),o={...b,pxPerMinute:Number(T.pxPerMinute??b.pxPerMinute)},g=Math.round(Number(T.chartHeight||360)),x=O.some(b9);U0(()=>{if(!E||!x)return;let Au=window.setInterval(()=>Z(Date.now()),1000);return()=>window.clearInterval(Au)},[E,x]);let lu=dS(f,r,Array.isArray(n)?n:[]),_u=r.map((Au)=>String(Au?.id||"")).filter(Boolean),$u=k.map((Au)=>String(Au.nodeId||"")).filter(Boolean),ju=X.map((Au)=>String(Au.nodeId||"")).filter(Boolean),zu=Array.from(new Set([...lu,..._u,...$u,...ju])),Wu={startY:0,endY:g,startMs:Number(I.startMs),endMs:Number(I.endMs)},P=Number(q?.endY||0)>0?q:Wu,e=(Au)=>{return G6(Au,I,g,T)<=Number(P.endY)&&Y9(Au,I,g,T)>=Number(P.startY)},uu=(Au)=>{let su=Jr(Au,I,g,T);return su>=Number(P.startY)&&su<=Number(P.endY)},Ku=new Set(zu.filter((Au)=>k.some((su)=>su.nodeId===Au&&e(su))||X.some((su)=>su.nodeId===Au&&uu(su)))),s=U?zu.filter((Au)=>Ku.has(Au)):zu,Nu=`${W9}px ${s.length>0?s.map(()=>`${j0}px`).join(" "):"minmax(160px, 1fr)"}`,Eu=Du(T.ticks).filter(Xu),Hu=String(J?.mode==="interval"?J?.interval?.procedureRunId||"":""),vu=String(J?.mode==="event"?J?.marker?.id||"":""),ul=()=>{let Au=H.current;if(!Au){W(Wu);return}let su=Math.max(0,Au.scrollTop-w9),Jf=Math.max(120,Au.clientHeight-w9),pu=Math.min(g,su+Jf),ff={startY:su,endY:pu,startMs:Number(I.startMs),endMs:Number(I.endMs)},rf=Math.max(0,Math.min(1,su/g)),ur=Math.max(rf,Math.min(1,pu/g)),nf=Math.max(1,Number(I.endMs)-Number(I.startMs));ff.startMs=Number(I.startMs)+nf*rf,ff.endMs=Number(I.startMs)+nf*ur,W(ff)};U0(()=>{let Au=H.current,su=window.setTimeout(ul,0);return Au?.addEventListener("scroll",ul),window.addEventListener("resize",ul),()=>{window.clearTimeout(su),Au?.removeEventListener("scroll",ul),window.removeEventListener("resize",ul)}},[E,I.startMs,I.endMs,g]);let mu=Math.max(0,zu.length-s.length),Fl=new Set(X.filter((Au)=>s.includes(String(Au.nodeId||""))&&uu(Au)).map((Au)=>String(Au.id))),Uf=new Map(X.map((Au)=>[String(Au.id),Au])),Ef=Du(m.arrows).filter((Au)=>{if(!Fl.has(String(Au.targetMarkerId||"")))return!1;if(String(Au.action||"")==="observe")return s.includes(String(Au.sourceNodeId||""));return Fl.has(String(Au.sourceMarkerId||""))}),lf=W9+Math.max(1,s.length)*j0,ol=(Au)=>{let su=vn(Au.target.value);if(typeof c==="function")c(su);window.setTimeout(ul,0)},Zf=()=>iY({title:`${f?.id||"pipeline"}-${E||"epoch"}-gantt`,meta:[`run ${E||"--"}`,`${qu(I.startMs)} -> ${qu(I.endMs)}`,`duration ${Br(I.durationMs)}`,`${o.label} / ${E9(o.pxPerMinute)} px/min`,`${s.length}/${zu.length} nodes`,`${X.length} markers`],visibleNodeIds:s,intervals:k,markers:X.filter((Au)=>s.includes(String(Au.nodeId||""))),arrows:Ef,ticks:Eu,bounds:I,chartHeight:g,backendLayout:T}),mf=Xu(S?.gantt?.diagnostics)?S.gantt.diagnostics:null;return G(F0,{title:"Epoch 甘特图",eyebrow:`${f?.id||"pipeline"} / ${u.length} epochs`,className:"pipeline-wide-panel",loading:i?.loading,actions:G("div",{className:"pipeline-gantt-actions"},G("select",{value:E,disabled:u.length===0,onChange:(Au)=>A(Au.target.value),"data-testid":"pipeline-epoch-select"},u.map((Au)=>G("option",{key:Au.runId,value:Au.runId},X9(u,Au)))),G("label",{className:"pipeline-gantt-toggle"},G("input",{type:"checkbox","data-testid":"pipeline-gantt-auto-hide-idle",checked:U,onChange:(Au)=>{N(Boolean(Au.target.checked)),window.setTimeout(ul,0)}}),G("span",null,"自动隐藏空闲列")),G("label",{className:"pipeline-gantt-scale"},G("span",null,G("b",null,"时间尺度"),G("em",{"data-testid":"pipeline-gantt-scale-label"},`${o.label} · ${E9(o.pxPerMinute)} px/min`)),G("input",{type:"range",min:0,max:100,step:0.01,value:V,onChange:ol,"aria-label":"调整甘特图时间尺度","data-testid":"pipeline-gantt-time-scale"}),G("small",null,G("span",null,"全局"),G("span",null,"细节"))),l?G("button",{type:"button",className:"ghost-btn",onClick:Zf,disabled:s.length===0,"data-testid":"pipeline-export-gantt"},"导出甘特图"):null,l?G(sr,{title:`Pipeline Epoch ${l.runId}`,data:l,onOpen:L,testId:"raw-pipeline-epoch-gantt"}):null)},!l?G(Qr,{title:"暂无 Epoch",text:"当前 pipeline 还没有完整运行记录。"}):k.length===0?G(Qr,{title:"暂无时间区间",text:"等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。"}):G("div",{className:"pipeline-gantt-wrap"},G("div",{className:`pipeline-gantt-detail-layout ${D?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-gantt-detail-layout","data-sidebar-open":D?"true":"false"},G("div",{className:"pipeline-gantt-main"},G("div",{className:"pipeline-gantt-main-head"},G("div",{className:"pipeline-gantt-meta"},G("span",null,`time ${qu(I.startMs)} -> ${qu(I.endMs)}`),G("span",null,`duration ${Br(I.durationMs)}`),G("span",null,`scale ${o.label} / ${E9(o.pxPerMinute)} px/min`),G("span",null,`layout ${Y}`),mf?G("span",null,`align ${mf.timeAxisAlignmentOk===!1?"check":"ok"}`):null,G("span",null,`visible ${s.length}/${zu.length} nodes`),S?G("span",null,`markers ${X.length}`):null,U&&mu>0?G("span",null,`hidden idle ${mu}`):null),!D?G("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!J?.mode,onClick:()=>h(!0),"data-testid":"pipeline-gantt-sidebar-toggle"},J?.mode?"展开详情":"点击甘特图元素展开详情"):null),G("div",{className:"pipeline-gantt-viewport",ref:H,"data-testid":"pipeline-epoch-gantt","data-pipeline-id":f?.id||"","data-run-id":E,"data-layout-source":Y,"data-start-ms":String(I.startMs),"data-end-ms":String(I.endMs),"data-chart-height":String(g)},G("div",{className:"pipeline-gantt-board",style:{gridTemplateColumns:Nu,minWidth:`${lf}px`}},G("div",{className:"pipeline-gantt-head time"},"Time"),s.length===0?G("div",{className:"pipeline-gantt-head empty"},"当前时间窗无工作节点"):s.map((Au)=>G("div",{key:`head-${Au}`,className:"pipeline-gantt-head node",title:Au,"data-testid":"pipeline-gantt-head-node","data-node-id":Au},G(XS,{value:Au}))),G("div",{className:"pipeline-gantt-time-axis",style:{height:`${g}px`}},Eu.map((Au)=>{let su=Mw(Au,I,g,T);return G("div",{key:`tick-${Au.ms}-${su}`,className:"pipeline-gantt-tick",style:{top:`${su}px`},"data-testid":"pipeline-gantt-tick","data-ms":String(Au.ms),"data-y":String(su)},G("b",null,qu(Au.ms)),G("span",null,`+${Br(Number(Au.offsetMs??Number(Au.ms)-Number(I.startMs)))}`))})),s.length>0?G("svg",{className:"pipeline-gantt-arrow-layer",width:s.length*j0,height:g,viewBox:`0 0 ${s.length*j0} ${g}`,style:{left:`${W9}px`,top:`${w9}px`,width:`${s.length*j0}px`,height:`${g}px`},"aria-hidden":"true"},G("defs",null,G("marker",{id:"pipeline-gantt-arrowhead",viewBox:"0 0 10 10",refX:9,refY:5,markerWidth:6,markerHeight:6,orient:"auto-start-reverse"},G("path",{d:"M 0 0 L 10 5 L 0 10 z",fill:"context-stroke"}))),Ef.map((Au)=>{let su=Uf.get(String(Au.targetMarkerId||""));if(!su)return null;let Jf=Uf.get(String(Au.sourceMarkerId||"")),pu=String(Jf?.nodeId||Au.sourceNodeId||""),ff=s.indexOf(pu),rf=s.indexOf(String(su.nodeId||""));if(ff<0||rf<0)return null;let ur=ff*j0+j0/2,nf=rf*j0+j0/2,Of=Jf?Jr(Jf,I,g,T):Jr(su,I,g,T),N0=Jr(su,I,g,T);return G("path",{key:Au.id,className:`pipeline-gantt-arrow ${String(Au.sourceKind||"").toLowerCase()} ${String(Au.status||"").toLowerCase()} ${String(Au.action||"").toLowerCase()}`,d:Rw(ur,Of,nf,N0),markerEnd:"url(#pipeline-gantt-arrowhead)","data-testid":String(Au.action||"")==="observe"?"pipeline-gantt-observation-arrow":"pipeline-gantt-arrow","data-source-node-id":String(Au.sourceNodeId||""),"data-target-node-id":String(Au.targetNodeId||""),"data-target-marker-id":String(Au.targetMarkerId||""),"data-action":String(Au.action||""),"data-source-y":String(Of),"data-target-y":String(N0)})})):null,s.length===0?G("div",{className:"pipeline-gantt-empty-col",style:{height:`${g}px`}},"滚动到有活动的时间段后,相关 node 列会自动出现。"):s.map((Au)=>{let su=k.filter((pu)=>pu.nodeId===Au),Jf=X.filter((pu)=>String(pu.nodeId||"")===Au);return G("div",{key:`col-${Au}`,className:"pipeline-gantt-node-col",style:{height:`${g}px`}},su.map((pu)=>{let ff=G6(pu,I,g,T),rf=Y9(pu,I,g,T),ur=Cw(pu,I,g,T),nf=String(pu.procedureRunId||`${Au}-${pu.startMs}`);return G("button",{key:nf,type:"button",className:`pipeline-gantt-bar ${pu.status} ${pu.live?"live":""} ${Hu===nf?"selected":""}`,style:{top:`${ff}px`,height:`${ur}px`},title:`${Au} ${pu.status} ${qu(pu.startedAt||pu.startMs)} -> ${qu(pu.finishedAt||pu.endMs)}`,onClick:()=>j(pu),"data-testid":"pipeline-gantt-line","data-node-id":Au,"data-procedure-run-id":String(pu.procedureRunId||""),"data-status":String(pu.status||""),"data-live":pu.live?"true":"false","data-start-ms":String(pu.startMs||""),"data-end-ms":String(pu.endMs||""),"data-y1":String(ff),"data-y2":String(rf),"data-natural-height":String(Math.max(0,rf-ff))},G("strong",null,pu.status||"working"),G("span",null,Br(pu.durationMs)))}),Jf.map((pu)=>G("button",{key:pu.id,type:"button",className:`pipeline-gantt-marker ${pu.kind} ${pu.tone||""} ${pu.status||""} ${vu===String(pu.id)?"selected":""}`,style:{top:`${Jr(pu,I,g,T)}px`},title:`${pu.label||"event"} / ${qu(pu.timestampIso||pu.timestamp||pu.ms)}`,onClick:()=>F(pu),"data-testid":pu.kind==="prompt"?"pipeline-gantt-prompt-marker":"pipeline-gantt-control-marker","data-marker-id":String(pu.id||""),"data-ms":String(pu.ms??pu.eventMs??""),"data-y":String(Jr(pu,I,g,T))})))})))),D?G(DS,{selection:J,runDetails:i,nodeDetails:y,nodeDetailsState:t,onRaw:L,onCollapse:()=>h(!1)}):null)))}function h0(){return{loading:!1,actionLoading:"",error:"",message:"",details:null,fetchedAt:null,appendPrompt:"",guidePrompt:"",modifyPrompt:"",approveReason:"",redoReason:""}}function Rn(){return{mode:"",runId:"",interval:null,marker:null}}function Z9(){return{runId:"",loading:!1,error:"",details:null,fetchedAt:null}}function V_(u,l){return`${u}/microservices/pipeline/proxy${l}`}function XY({activeRun:u,pipelineRuns:l,selectedRunId:f,onRunChange:r,selectedNodeId:n,selectedNodeConfig:i,selectedNodeRuntime:y,control:t,onControlChange:_,onFetch:c,onAction:A,onRaw:j,onCollapse:F}){let J=String(u?.runId||""),Q=String(y?.status||"pending"),w=!J||!n||t.loading||Boolean(t.actionLoading),L=(N)=>(q)=>_({[N]:q.target.value,error:"",message:""}),U=l.length>0?l:u?[u]:[];return G("aside",{className:"pipeline-node-control","data-testid":"pipeline-node-control"},G("div",{className:"pipeline-node-control-head"},G("div",null,G("p",{className:"panel-eyebrow"},"Manual Node Control"),G(nl,{title:n||"点击控制图中的 node",level:3,loading:t.loading||Boolean(t.actionLoading)})),G("div",{className:"pipeline-node-control-head-actions"},n?G(bn,{status:Q},Q):G(bn,{status:"pending"},"idle"),G("button",{type:"button",className:"ghost-btn mini",onClick:F,"data-testid":"pipeline-node-sidebar-collapse"},"收起"))),G("div",{className:"pipeline-control-runbar"},G("label",null,G("span",null,"目标 run"),G("select",{value:J||f,disabled:U.length===0,onChange:(N)=>r(N.target.value),"data-testid":"pipeline-node-run-select"},U.map((N)=>G("option",{key:N.runId,value:N.runId},`${N.runId||"--"} / ${N.status||"--"}`)))),G("button",{type:"button",className:"ghost-btn",disabled:w,onClick:c,"data-testid":"pipeline-node-fetch"},t.loading?"抓取中":"抓取过程"),t.details?G(sr,{title:`Pipeline Node ${n}`,data:t.details,onOpen:j,testId:"raw-pipeline-node-control"}):null),G("div",{className:"pipeline-control-meta"},G("span",null,G("b",null,"kind"),String(i?.kind||"--")),G("span",null,G("b",null,"procedure"),String(y?.currentProcedureRunId||"--")),G("span",null,G("b",null,"attempts"),String(y?.attempts??"--")),G("span",null,G("b",null,"updated"),qu(u?.updatedAt))),!n?G(Qr,{title:"未选择 node",text:"点击 React Flow 控制图中的任意 node 后,可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。"}):null,G(il,{error:t.error,wide:!0}),G("div",{className:"pipeline-control-actions"},G("label",null,G("span",null,"实时追加 prompt(仅 running node)"),G("textarea",{value:t.appendPrompt,onChange:L("appendPrompt"),placeholder:"让当前执行中的 agent 继续、补充检查或调整当前步骤...",rows:4,disabled:!n,"data-testid":"pipeline-node-append-input"}),G("button",{type:"button",className:"primary-btn compact",disabled:w||!String(t.appendPrompt||"").trim(),onClick:()=>A("append"),"data-testid":"pipeline-node-append-button"},t.actionLoading==="append"?"追加中":"追加到运行中 node")),G("label",null,G("span",null,"下次尝试引导 prompt"),G("textarea",{value:t.guidePrompt,onChange:L("guidePrompt"),placeholder:"给该 node 下一次 attempt 的执行提示;不会立即打断当前 session。",rows:4,disabled:!n,"data-testid":"pipeline-node-guide-input"}),G("button",{type:"button",className:"ghost-btn compact",disabled:w||!String(t.guidePrompt||"").trim(),onClick:()=>A("guide"),"data-testid":"pipeline-node-guide-button"},t.actionLoading==="guide"?"下发中":"下发 guide")),G("label",null,G("span",null,"完成后增量修改 prompt"),G("textarea",{value:t.modifyPrompt,onChange:L("modifyPrompt"),placeholder:"在该 node 已完成结果基础上追加修改要求;runner 会重跑目标 node,并保留同 node 既有 OA 输出作为上下文。",rows:4,disabled:!n,"data-testid":"pipeline-node-modify-input"}),G("button",{type:"button",className:"ghost-btn compact",disabled:w||!String(t.modifyPrompt||"").trim(),onClick:()=>A("modify"),"data-testid":"pipeline-node-modify-button"},t.actionLoading==="modify"?"排队中":"增量修改 node")),G("label",null,G("span",null,"Monitor 审核通过原因"),G("textarea",{value:t.approveReason,onChange:L("approveReason"),placeholder:"当流程配置开启 monitor 审核时,记录审核通过原因并释放后续 node。",rows:3,disabled:!n,"data-testid":"pipeline-node-approve-input"}),G("button",{type:"button",className:"primary-btn compact",disabled:w||!String(t.approveReason||"").trim(),onClick:()=>A("approve"),"data-testid":"pipeline-node-approve-button"},t.actionLoading==="approve"?"提交中":"审核通过")),G("label",null,G("span",null,"重做 / restart 原因"),G("textarea",{value:t.redoReason,onChange:L("redoReason"),placeholder:"说明为什么需要重做;runner 会重置目标 node 以及非 rework 下游 node。",rows:4,disabled:!n,"data-testid":"pipeline-node-redo-input"}),G("button",{type:"button",className:"danger-btn compact",disabled:w||!String(t.redoReason||"").trim(),onClick:()=>A("redo"),"data-testid":"pipeline-node-redo-button"},t.actionLoading==="redo"?"排队中":"重做 node"))),G("div",{className:"pipeline-control-evidence"},G("strong",null,"Node 过程索引"),G(OY,{details:t.details,selectedNodeId:n,selectedNodeRuntime:y,control:t,onRaw:j})))}function xw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((fu)=>fu.id==="pipeline")||null,[n,i]=af({loading:!1,error:"",health:null,snapshot:null,oaDiagnostics:null,minimaxQuota:null,refreshedAt:null}),[y,t]=af(""),[_,c]=af(""),[A,j]=af(""),[F,J]=af(h0()),[Q,w]=af({}),[L,U]=af(Rn()),[N,q]=af(Z9()),[W,z]=af(P_),[Z,H]=af(!1),[E,D]=af(!1),h=xn(0),{addNotification:V}=Xf(),S=xn(!1),p=xn(0),O=xn(""),m=xn({}),X=xn(""),v=xn("");async function T(fu={}){let Bu=fu.silent===!0;if(!r)return;if(S.current)return;S.current=!0;let Yu=h.current+1;if(h.current=Yu,!Bu)i((au)=>({...au,loading:!0,error:""}));try{let au=`__unideskArrayLimit=registry.components:80,runs:${JS}`,[_l,Pl,yl]=await Promise.all([Mn(`${f}/microservices/pipeline/proxy/api/snapshot?${au}`,{cache:"no-store"}),Mn(`${f}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`,{cache:"no-store"}).catch((Xr)=>({ok:!1,error:Ou(Xr,"OA event flow diagnostics failed")})),Mn(`${f}/microservices/pipeline/proxy/api/model-quota/minimax`,{cache:"no-store"}).catch((Xr)=>({ok:!1,error:Ou(Xr,"MiniMax quota failed")}))]);if(Yu!==h.current)return;let Qf={ok:_l?.ok!==!1,service:"pipeline-v2-control snapshot"};i({loading:!1,error:"",health:Qf,snapshot:_l,oaDiagnostics:Pl,minimaxQuota:yl,refreshedAt:new Date})}catch(au){if(Yu!==h.current)return;i((_l)=>({..._l,loading:!1,error:Ou(au,"Pipeline 加载失败")}))}finally{S.current=!1}}U0(()=>{if(T(),!r)return;let fu=()=>{if(A6())T({silent:!0})},Bu=window.setInterval(()=>{fu()},uw),Yu=()=>{if(A6())fu()};return document.addEventListener("visibilitychange",Yu),()=>{window.clearInterval(Bu),document.removeEventListener("visibilitychange",Yu)}},[r?.id,r?.runtime?.providerStatus,f]);let Y=SS(r),k=pS(r),I=YS(r),b=n.snapshot||{},o=n.oaDiagnostics||null,g=n.minimaxQuota||null,{components:x,pipelines:lu,runs:_u}=mS(b),$u=String(_u[0]?.pipelineId||""),ju=($u?lu.find((fu)=>String(fu.id||"")===$u):null)||lu[0]||{},zu=lu.find((fu)=>String(fu.id||"")===y)||ju,Wu=String(zu.id||""),P=Hw(zu),e=M9(zu),uu=Qw(_u,Wu),Ku=tY(_u,Wu),s=Ku.find((fu)=>String(fu?.runId||"")===_)||uu,Nu=String(N.runId||"")===String(s?.runId||"")?RS(N.details):null,Eu=xS(s,Nu),Hu=String(Eu?.runId||""),vu=P.find((fu)=>String(fu?.id||"")===A)||null,ul=A?Bw(Eu,A):null,mu=CS(_u),Fl=kS(x),Uf=Number(n.health?.components)||$w(b,"registry.components",x.length),Ef=$w(b,"runs",_u.length),lf=jw(zu,Eu,x),ol={nodes:lf.nodes.map((fu)=>fu.id===A?{...fu,selected:!0,className:`${fu.className||""} selected-control-node`}:fu),edges:lf.edges},Zf=lu.map((fu)=>{let Bu=String(fu.id||"pipeline"),Yu=Qw(_u,Bu);return{title:`${Bu}-${Yu?.runId||"snapshot"}`,flow:jw(fu,Yu,x)}}),mf=String(L?.runId||Hu||""),Au=String(L?.interval?.nodeId||L?.marker?.nodeId||""),su=mf&&Au?Q[T9(mf,Au)]||null:null,Jf=q6(F.details,mf,Au),pu=q6(su?.details,mf,Au)||Jf,ff=mf&&Au?{...Xu(su)?su:{},runId:mf,nodeId:Au,details:pu,loading:Boolean(su?.loading)||!pu&&Boolean(F.loading)&&A===Au,error:String(su?.error||""),fetchedAt:su?.fetchedAt||(Jf?F.fetchedAt:null)}:null,rf=Ku.map((fu)=>String(fu?.runId||"")).filter(Boolean).join("|"),ur=P.map((fu)=>String(fu?.id||"")).filter(Boolean).join("|");U0(()=>{X.current=A},[A]),U0(()=>{v.current=Hu},[Hu]),U0(()=>{if(!_||rf.split("|").includes(_))return;c("")},[_,rf]),U0(()=>{if(!A||ur.split("|").includes(A))return;j(""),J(h0()),U(Rn()),H(!1),D(!1)},[A,ur]),U0(()=>{if(!A)H(!1)},[A]),U0(()=>{if(!L.mode)D(!1)},[L.mode]);async function nf(fu=Hu,Bu={}){if(!fu){q(Z9());return}let Yu=vn(Bu.scale??W??P_),au=`${fu}:timeline`;if(O.current===au)return;O.current=au;let _l=Bu.silent===!0,Pl=p.current+1;p.current=Pl,q((yl)=>({runId:fu,scale:Yu,loading:!_l||String(yl.runId||"")!==fu||!yl.details,error:"",details:_l&&yl.runId===fu?yl.details:yl.runId===fu?yl.details:null,fetchedAt:yl.runId===fu?yl.fetchedAt:null}));try{let[yl,Qf]=await Promise.all([Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}?tail=160&view=timeline`),{cache:"no-store",strictJson:!0}),Mn(V_(f,`/api/runs/${encodeURIComponent(fu)}`),{cache:"no-store"}).catch((Xr)=>({ok:!1,runSummaryError:Ou(Xr,"抓取评分失败")}))]);if(Pl!==p.current)return;q({runId:fu,scale:Yu,loading:!1,error:"",details:{...yl,run:Xu(Qf?.run)?Qf.run:void 0,runSummaryError:Qf?.runSummaryError},fetchedAt:new Date})}catch(yl){if(Pl!==p.current)return;q((Qf)=>({runId:fu,scale:Yu,loading:!1,error:Ou(yl,"抓取 epoch 执行过程失败"),details:Qf.runId===fu?Qf.details:null,fetchedAt:Qf.runId===fu?Qf.fetchedAt:null}))}finally{if(O.current===au)O.current=""}}function Of(fu,Bu,Yu){let au=T9(fu,Bu);w((_l)=>{let Pl={..._l,[au]:{...Xu(_l?.[au])?_l[au]:{},runId:fu,nodeId:Bu,...Yu}},yl=Object.keys(Pl);if(yl.length>32)for(let Qf of yl.slice(0,yl.length-32))delete Pl[Qf];return Pl})}async function N0(fu,Bu){if(!fu||!Bu)return;let Yu=T9(fu,Bu),au=Number(m.current?.[Yu]||0)+1;m.current={...m.current,[Yu]:au},Of(fu,Bu,{loading:!0,error:""});try{let _l=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}/nodes/${encodeURIComponent(Bu)}?tail=160`),{cache:"no-store",strictJson:!0});if(Number(m.current?.[Yu]||0)!==au)return;let Pl=new Date;if(Of(fu,Bu,{loading:!1,details:_l,fetchedAt:Pl,error:""}),X.current===Bu&&v.current===fu)J((yl)=>({...yl,loading:!1,details:_l,fetchedAt:Pl,error:""}))}catch(_l){if(Number(m.current?.[Yu]||0)!==au)return;Of(fu,Bu,{loading:!1,error:Ou(_l,"抓取 Gantt node 详情失败")})}}U0(()=>{if(!Hu){q(Z9());return}nf(Hu);let fu=()=>{if(A6())nf(Hu,{silent:!0})},Bu=window.setInterval(()=>{fu()},uw),Yu=()=>{if(A6())fu()};return document.addEventListener("visibilitychange",Yu),()=>{window.clearInterval(Bu),document.removeEventListener("visibilitychange",Yu)}},[Hu,f]);async function lr(fu=Hu,Bu=A){if(!fu||!Bu){J((Yu)=>({...Yu,error:"请先选择 run 和 node",message:""}));return}J((Yu)=>({...Yu,loading:!0,error:"",message:""}));try{let Yu=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(fu)}/nodes/${encodeURIComponent(Bu)}?tail=160`),{cache:"no-store",strictJson:!0}),au=new Date;J((_l)=>({..._l,loading:!1,details:Yu,fetchedAt:au,error:""})),Of(fu,Bu,{loading:!1,details:Yu,fetchedAt:au,error:""})}catch(Yu){J((au)=>({...au,loading:!1,error:Ou(Yu,"抓取 node 执行过程失败")}))}}async function k0(fu){let Bu=String(fu?.runId||Hu||""),Yu=String(fu?.nodeId||"");if(U({mode:"interval",runId:Bu,interval:fu,marker:null}),D(!0),!Bu||!Yu)return;if(Bu!==Hu)c(Bu);j(Yu),J(h0()),nf(Bu,{silent:!0}),N0(Bu,Yu)}async function Ul(fu){let Bu=String(fu?.runId||Hu||""),Yu=String(fu?.nodeId||"");if(U({mode:"event",runId:Bu,interval:null,marker:fu}),D(!0),!Bu)return;if(Bu!==Hu)c(Bu);if(nf(Bu,{silent:!0}),!Yu)return;j(Yu),J(h0()),N0(Bu,Yu)}async function bi(fu){if(!Hu||!A){J((au)=>({...au,error:"请先选择 run 和 node",message:""}));return}let Bu=fu==="append"?"prompts":fu,Yu=fu==="append"?F.appendPrompt:fu==="guide"?F.guidePrompt:fu==="modify"?F.modifyPrompt:fu==="approve"?F.approveReason:F.redoReason;if(!String(Yu||"").trim()){J((au)=>({...au,error:"操作内容不能为空",message:""}));return}J((au)=>({...au,actionLoading:fu,error:"",message:""}));try{let au=fu==="redo"||fu==="approve"?{reason:Yu,source:"unidesk-frontend",sourceKind:"webui"}:{prompt:Yu,source:"unidesk-frontend",sourceKind:"webui"},_l=await Mn(V_(f,`/api/node-control/runs/${encodeURIComponent(Hu)}/nodes/${encodeURIComponent(A)}/${Bu}`),{method:"POST",body:JSON.stringify(au)});if(J((yl)=>({...yl,actionLoading:"",details:_l,fetchedAt:new Date,appendPrompt:fu==="append"?"":yl.appendPrompt,guidePrompt:fu==="guide"?"":yl.guidePrompt,modifyPrompt:fu==="modify"?"":yl.modifyPrompt,approveReason:fu==="approve"?"":yl.approveReason,redoReason:fu==="redo"?"":yl.redoReason,message:fu==="append"?"已追加到运行中 node":fu==="guide"?"已下发 guide,等待 runner 处理":fu==="modify"?"已排队增量修改命令":fu==="approve"?"已提交审核通过决策":"已排队重做命令"})),V("success",fu==="append"?"已追加到运行中 node":fu==="guide"?"已下发 guide,等待 runner 处理":fu==="modify"?"已排队增量修改命令":fu==="approve"?"已提交审核通过决策":"已排队重做命令"),await lr(Hu,A),await nf(Hu,{silent:!0}),fu!=="append")await T()}catch(au){J((_l)=>({..._l,actionLoading:"",error:Ou(au,"node 控制操作失败")}))}}if(!r)return G(Qr,{title:"Pipeline 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=pipeline"});return G("div",{className:"pipeline-page","data-testid":"pipeline-page"},G(F0,{title:"Pipeline v2 工作台",eyebrow:"D601 Snapshot 用户服务",loading:n.loading,actions:G("div",{className:"panel-actions"},G("button",{type:"button",className:"ghost-btn",onClick:T,disabled:n.loading,"data-testid":"pipeline-refresh-button"},n.loading?"刷新中":"刷新"),G(sr,{title:"Pipeline 用户服务",data:r,onOpen:l,testId:"raw-pipeline-service"}))},G("div",{className:"pipeline-hero"},G("div",null,G("div",{className:"node-version-line"},G(bn,{status:Y.providerStatus==="online"?"online":"warn"},Y.providerStatus||"unknown"),G("span",null,r.providerId),G("span",null,I.public?"公网暴露":"仅 UniDesk frontend 代理访问")),G("p",{className:"muted paragraph"},r.description)),G("div",{className:"microservice-ref-card"},G("span",null,"Repo"),G("strong",null,k.url||"--"),G("code",null,k.commitId||"--")),G("div",{className:"microservice-ref-card"},G("span",null,"D601 Docker"),G("strong",null,`${I.nodeBindHost||"--"}:${I.nodePort||"--"}`),G("code",null,`${k.composeFile||"--"} / ${k.composeService||"--"}`))),G(il,{error:n.error,wide:!0})),G("div",{className:"pipeline-grid"},G(F0,{title:"控制图",eyebrow:`${zu.id||"pipeline"} / run ${Eu?.status||"--"}`,className:"pipeline-wide-panel",loading:n.loading,actions:G("div",{className:"pipeline-toolbar"},G("select",{value:Wu,disabled:lu.length===0,onChange:(fu)=>{t(fu.target.value),c(""),j(""),J(h0()),U(Rn()),H(!1),D(!1)},"data-testid":"pipeline-select"},lu.map((fu)=>G("option",{key:fu.id,value:fu.id},fu.id||fu.key))),G("select",{value:Hu,disabled:Ku.length===0,onChange:(fu)=>{if(c(fu.target.value),J(h0()),U(Rn()),H(!1),D(!1),A)lr(fu.target.value,A)},"data-testid":"pipeline-run-select"},Ku.map((fu)=>G("option",{key:fu.runId,value:fu.runId},X9(Ku,fu)))),G("button",{type:"button",className:"ghost-btn",disabled:ol.nodes.length===0,onClick:()=>Xw(ol,`${zu.id||"pipeline"}-${Eu?.runId||"snapshot"}`),"data-testid":"pipeline-export-graph"},"导出渲染图"),G("button",{type:"button",className:"ghost-btn",disabled:Zf.every((fu)=>fu.flow.nodes.length===0),onClick:()=>yY(Zf),"data-testid":"pipeline-export-all-graphs"},"批量导出"))},P.length===0?G(Qr,{title:"暂无控制图",text:"等待 D601 pipeline backend 返回 config.nodes / config.edges"}):G("div",{className:`pipeline-control-shell ${Z?"detail-open":"detail-collapsed"}`,"data-testid":"pipeline-control-shell","data-sidebar-open":Z?"true":"false"},G("div",{className:"pipeline-flow-frame","data-testid":"pipeline-react-flow"},G(IW,{nodes:ol.nodes,edges:ol.edges,nodeTypes:wS,edgeTypes:WS,fitView:!0,fitViewOptions:{padding:0.18},nodesDraggable:!1,nodesConnectable:!1,elementsSelectable:!0,minZoom:0.25,maxZoom:1.4,proOptions:{hideAttribution:!0},onNodeClick:(fu,Bu)=>{let Yu=String(Bu.id);if(j(Yu),J(h0()),H(!0),Hu)lr(Hu,Yu)}},G(sW,{gap:22,size:1,color:"rgba(215, 161, 58, 0.24)"}),G(oW,{showInteractive:!1})),!Z?G("button",{type:"button",className:"pipeline-sidecar-tab right",disabled:!A,onClick:()=>H(!0),"data-testid":"pipeline-node-sidebar-toggle"},A?"展开 node 控制":"点击 node 展开控制"):null),Z?G(XY,{activeRun:Eu,pipelineRuns:Ku,selectedRunId:_,onRunChange:(fu)=>{if(c(fu),J(h0()),U(Rn()),A)lr(fu,A)},selectedNodeId:A,selectedNodeConfig:vu,selectedNodeRuntime:ul,control:F,onControlChange:(fu)=>J((Bu)=>({...Bu,...fu})),onFetch:()=>lr(),onAction:bi,onRaw:l,onCollapse:()=>H(!1)}):null),G("div",{className:"pipeline-flow-summary"},G("span",null,`${ol.nodes.length} nodes`),G("span",null,`${ol.edges.length} edges`),G("span",null,`${lu.length} pipelines`),G("span",null,`source config+components(${x.length})`),G("span",null,`run ${Eu?.runId||"--"}`),G("span",null,`score ${D9(Eu)}`),G("span",null,A?`selected ${A}`:"click node to control"))),G(DY,{epochs:Ku,activeRun:Eu,activePipeline:zu,pipelineNodes:P,pipelineEdges:e,selection:L,detailOpen:E,onDetailOpenChange:D,runDetails:N,nodeDetails:pu,nodeDetailsState:ff,ganttScale:W,onGanttScaleChange:z,onIntervalSelect:k0,onMarkerSelect:Ul,onRunChange:(fu)=>{if(c(fu),J(h0()),U(Rn()),D(!1),A)lr(fu,A)},onRaw:l}),G(F0,{title:"观测指标",eyebrow:n.refreshedAt?`Updated ${tl(n.refreshedAt)}`:"Snapshot",loading:n.loading},G("div",{className:"metric-grid"},G(pf,{label:"Health",value:n.health?.ok?"OK":"--",hint:n.health?.service||"D601 /health",tone:n.health?.ok?"ok":"warn"}),G(pf,{label:"组件",value:Uf,hint:"components registry",tone:b?.registry?.ok===!1?"warn":"ok"}),G(pf,{label:"Pipeline",value:lu.length,hint:`${P.length} nodes / ${e.length} edges`}),G(pf,{label:"运行记录",value:Ef,hint:`${mu.succeeded||0} succeeded / ${mu.running||0} running`}),G(pf,{label:"OA 记录",value:Array.isArray(uu?.submissions)?uu.submissions.length:0,hint:uu?.runId||"latest run"}),G(pf,{label:"Procedure",value:Array.isArray(uu?.procedureRuns)?uu.procedureRuns.length:0,hint:uu?.status||"no run"}),G(pf,{label:"Score",value:D9(Eu),hint:Eu?.runId||"selected epoch",tone:x9(Eu)})),G("div",{className:"panel-actions inline-actions"},G(sr,{title:"Pipeline Snapshot",data:b,onOpen:l,testId:"raw-pipeline-snapshot"}))),G(F0,{title:"评分器",eyebrow:Eu?.runId||"selected epoch",loading:n.loading},G(vS,{run:Eu,onRaw:l})),G(F0,{title:"MiniMax 限额",eyebrow:"model/minimax-m27 quota",loading:n.loading},G(VY,{quota:g,onRaw:l})),G(F0,{title:"OA 事件流",eyebrow:"100% event-driven diagnostics",className:"pipeline-wide-panel",loading:n.loading},G(BY,{diagnostics:o,onRaw:l})),G(F0,{title:"组件矩阵",eyebrow:`${Fl.length} classes`,loading:n.loading},Fl.length===0?G(Qr,{title:"暂无组件",text:"等待 D601 pipeline backend 返回 registry.components"}):G("div",{className:"component-strata"},Fl.map((fu)=>G("article",{key:fu.name,className:"component-stratum"},G("span",null,fu.name),G("strong",null,fu.count)))),G("div",{className:"pipeline-component-list"},x.slice(0,12).map((fu)=>G("span",{key:fu.key,className:"data-chip"},G("b",null,fu.componentClass||"--"),G("span",null,fu.id||fu.key||"--"))))),G(F0,{title:"Epoch 列表",eyebrow:`${Ku.length}/${Ef} preview`,loading:n.loading},Ku.length===0?G(Qr,{title:"暂无运行记录",text:"当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。"}):G("div",{className:"pipeline-run-list"},Ku.map((fu)=>{let Bu=String(fu?.runId||"")===Hu?Eu:fu;return G("article",{key:fu.runId,className:`pipeline-run-card ${String(fu.runId||"")===Hu?"active":""}`,role:"button",tabIndex:0,onClick:()=>{c(String(fu.runId||"")),U(Rn())},onKeyDown:(Yu)=>{if(Yu.key==="Enter"||Yu.key===" ")c(String(fu.runId||"")),U(Rn())}},G("div",{className:"node-card-head"},G("strong",null,X9(Ku,fu)),G(bn,{status:fu.status},fu.status||"--")),G("div",{className:"docker-meta compact"},G("span",null,Bu?.pipelineId||"--"),G("span",null,`nodes ${Array.isArray(Bu?.nodes)?Bu.nodes.length:0}`),G("span",null,`oa ${Array.isArray(Bu?.submissions)?Bu.submissions.length:0}`),G("span",null,`procedures ${Array.isArray(Bu?.procedureRuns)?Bu.procedureRuns.length:0}`),G(bS,{run:Bu})),G("p",{className:"muted paragraph"},N6(Bu?.task)),G("span",{className:"pipeline-run-time"},qu(Bu?.updatedAt)))}))),G(F0,{title:"运行材料索引",eyebrow:Eu?.runId||"selected epoch",className:"pipeline-wide-panel",loading:n.loading},G(HY,{activeRun:Eu,onRaw:l}))))}var H6=Pu(Jl(),1);var tu=H6.default.createElement,{useEffect:SY}=H6.default,Z6=H6.default.useState,k9={id:"",sequenceNo:"",contractNo:"",name:"",currentStatus:"",pending:"",paymentStatus:"",notes:""};function YY({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return tu("span",{className:`status-badge ${f}`},l||u||"unknown")}function O6({label:u,value:l,hint:f,tone:r}){return tu("article",{className:`metric-card ${r||""}`},tu("div",{className:"metric-label"},u),tu("div",{className:"metric-value"},l),tu("div",{className:"metric-hint"},f))}function I9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return tu("section",{className:`panel ${n||""}`},tu("div",{className:"panel-head"},tu("div",null,l?tu("p",{className:"panel-eyebrow"},l):null,tu(nl,{title:u,loading:i})),f?tu("div",{className:"panel-actions"},f):null),tu("div",{className:"panel-body"},r))}function hw({title:u,data:l,onOpen:f,testId:r}){return tu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function bw({title:u,text:l}){return tu("div",{className:"empty-state"},tu("strong",null,u),tu("span",null,l))}function pY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function mY(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function PY(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function Ny(u,l){return`${u}/microservices/project-manager/proxy${l}`}function CY(u){return{id:String(u.id||""),sequenceNo:u.sequenceNo===null||u.sequenceNo===void 0?"":String(u.sequenceNo),contractNo:String(u.contractNo||""),name:String(u.name||""),currentStatus:String(u.currentStatus||""),pending:String(u.pending||""),paymentStatus:String(u.paymentStatus||""),notes:String(u.notes||"")}}function MY(u){return{sequenceNo:u.sequenceNo===""?null:Number(u.sequenceNo),contractNo:String(u.contractNo||"").trim(),name:String(u.name||"").trim(),currentStatus:String(u.currentStatus||"").trim(),pending:String(u.pending||"").trim(),paymentStatus:String(u.paymentStatus||"").trim(),paymentRatio:String(u.paymentStatus||"").trim(),notes:String(u.notes||"").trim()}}function g9(u){return String(u||"item").replace(/[^A-Za-z0-9_-]+/g,"-")}function RY(u){let l=new Uint8Array(u),f="",r=32768;for(let n=0;ntu("tr",{key:n.id,className:l===n.id?"active-row":"","data-testid":`project-manager-row-${g9(n.id)}`},tu("td",null,n.sequenceNo??"--"),tu("td",null,tu("strong",null,n.contractNo||"--"),tu("code",null,n.id||"--")),tu("td",null,tu("strong",null,n.name||"--"),tu("span",{className:"muted block"},n.sourceFile||"--")),tu("td",null,n.currentStatus||"--"),tu("td",null,tu("span",{className:"preline"},n.pending||"--")),tu("td",null,tu(YY,{status:Number(n.paymentRatio||0)>=1?"online":"warn"},n.paymentStatus||"--")),tu("td",null,n.notes||"--"),tu("td",null,tu("div",{className:"inline-actions"},tu("button",{type:"button",className:"ghost-btn",onClick:()=>f(n),"data-testid":`project-manager-edit-${g9(n.id)}`},"编辑"),tu(hw,{title:`Project ${n.contractNo||n.id}`,data:n,onOpen:r,testId:`raw-project-${g9(n.id)}`}))))))))}function vw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((E)=>E.id==="project-manager")||null,[n,i]=Z6({loading:!1,saving:!1,importing:!1,exporting:!1,error:"",notice:"",health:null,list:null,refreshedAt:null}),[y,t]=Z6({...k9}),[_,c]=Z6(""),[A,j]=Z6("all"),{addNotification:F}=Xf();async function J(E=_,D=A){if(!r)return;i((h)=>({...h,loading:!0,error:""}));try{let h=new URLSearchParams({pageSize:"200",status:D});if(E.trim())h.set("q",E.trim());let[V,S]=await Promise.all([Tu(`${f}/microservices/project-manager/health`),Tu(Ny(f,`/api/projects?${h.toString()}`))]);i((p)=>({...p,loading:!1,health:V,list:S,refreshedAt:new Date,error:""}))}catch(h){i((V)=>({...V,loading:!1,error:Ou(h,"Project Manager 加载失败")}))}}SY(()=>{J()},[r?.id,r?.runtime?.providerStatus]);async function Q(E){E.preventDefault(),i((D)=>({...D,saving:!0,error:"",notice:""}));try{let D=MY(y);if(y.id)await Tu(Ny(f,`/api/projects/${encodeURIComponent(y.id)}`),{method:"PUT",body:JSON.stringify(D)});else await Tu(Ny(f,"/api/projects"),{method:"POST",body:JSON.stringify(D)});let h=y.id?"项目已更新":"项目已创建";i((V)=>({...V,saving:!1,notice:h})),F("success",h),await J()}catch(D){i((h)=>({...h,saving:!1,error:Ou(D,"保存项目失败")}))}}async function w(){if(!y.id)return;if(!window.confirm(`删除项目 ${y.contractNo||y.name||y.id} ?`))return;i((E)=>({...E,saving:!0,error:"",notice:""}));try{await Tu(Ny(f,`/api/projects/${encodeURIComponent(y.id)}`),{method:"DELETE"}),t({...k9});let E="项目已删除";i((D)=>({...D,saving:!1,notice:E})),F("success",E),await J()}catch(E){i((D)=>({...D,saving:!1,error:Ou(E,"删除项目失败")}))}}async function L(E){let D=E.target.files?.[0];if(!D)return;i((h)=>({...h,importing:!0,error:"",notice:""}));try{let h=RY(await D.arrayBuffer()),S=`Excel 已导入 ${(await Tu(Ny(f,"/api/import/excel"),{method:"POST",body:JSON.stringify({fileName:D.name,contentBase64:h,replace:!1})})).imported||0} 条项目`;i((p)=>({...p,importing:!1,notice:S})),F("success",S),E.target.value="",await J()}catch(h){i((V)=>({...V,importing:!1,error:Ou(h,"Excel 导入失败")}))}}async function U(){i((E)=>({...E,exporting:!0,error:""}));try{let E=await cJ(Ny(f,"/api/projects/export.xlsx")),D=URL.createObjectURL(E),h=document.createElement("a");h.href=D,h.download=`project-manager-${x7()}.xlsx`,document.body.appendChild(h),h.click(),h.remove(),URL.revokeObjectURL(D),i((V)=>({...V,exporting:!1,notice:"Excel 已导出"}))}catch(E){i((D)=>({...D,exporting:!1,error:Ou(E,"Excel 导出失败")}))}}if(!r)return tu(bw,{title:"Project Manager 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=project-manager"});let N=pY(r),q=PY(r),W=mY(r),z=Array.isArray(n.list?.projects)?n.list.projects:[],Z=n.list?.summary||{},H=n.health||{};return tu("div",{className:"project-manager-page","data-testid":"project-manager-page"},tu(I9,{title:"项目管理工作台",eyebrow:"Main Server PostgreSQL 用户服务",loading:n.loading||n.exporting,actions:tu("div",{className:"panel-actions"},tu("button",{type:"button",className:"ghost-btn",disabled:n.loading,onClick:()=>J(),"data-testid":"project-manager-refresh-button"},n.loading?"刷新中":"刷新"),tu("button",{type:"button",className:"ghost-btn",disabled:n.exporting,onClick:U,"data-testid":"project-manager-export-button"},n.exporting?"导出中":"导出 Excel"),tu(hw,{title:"Project Manager 用户服务",data:r,onOpen:l,testId:"raw-project-manager-service"}))},tu("div",{className:"project-manager-hero"},tu(O6,{label:"项目总数",value:Z.total??z.length,hint:`PG 表 ${H.storage?.table||"project_manager_projects"}`,tone:"ok"}),tu(O6,{label:"进行中",value:Z.active??"--",hint:"当前状态未完全完成"}),tu(O6,{label:"已完成",value:Z.completed??"--",hint:"按 完成 关键字统计",tone:"ok"}),tu(O6,{label:"未全款",value:Z.unpaid??"--",hint:"付款比例 < 1",tone:Number(Z.unpaid||0)>0?"warn":"ok"})),tu(il,{error:n.error}),n.notice?tu("div",{className:"form-success"},n.notice):null),tu("div",{className:"project-manager-hero"},tu("div",{className:"microservice-ref-card"},tu("span",null,"Repo"),tu("strong",null,q.url||"--"),tu("code",null,q.commitId||"--")),tu("div",{className:"microservice-ref-card"},tu("span",null,"Main Server Docker"),tu("strong",null,`${W.nodeBindHost||"--"}:${W.nodePort||"--"}`),tu("code",null,`${q.composeService||"--"} / ${q.containerName||"--"}`)),tu("div",{className:"microservice-ref-card"},tu("span",null,"Runtime"),tu("strong",null,N.providerName||r.providerId),tu("code",null,`Health ${H.ok?"OK":"--"} / ${n.refreshedAt?tl(n.refreshedAt):"--"}`)),tu("div",{className:"microservice-ref-card"},tu("span",null,"Import Source"),tu("strong",null,"D601 WeChat Excel"),tu("code",null,"合作项目列表_I_20260309.xlsx"))),tu("div",{className:"project-manager-layout"},tu(I9,{title:"项目清单",eyebrow:"CRUD + Excel Export",loading:n.loading||n.importing||n.exporting,actions:tu("div",{className:"inline-actions project-manager-filters"},tu("input",{value:_,onChange:(E)=>c(E.target.value),placeholder:"搜索合同号 / 项目名称 / 状态","data-testid":"project-manager-search"}),tu("select",{value:A,onChange:(E)=>{j(E.target.value),J(_,E.target.value)},"data-testid":"project-manager-status-filter"},tu("option",{value:"all"},"全部"),tu("option",{value:"active"},"进行中"),tu("option",{value:"completed"},"已完成"),tu("option",{value:"unpaid"},"未全款")),tu("button",{type:"button",className:"ghost-btn",onClick:()=>J(_,A)},"筛选"))},tu(xY,{projects:z,activeId:y.id,onSelect:(E)=>t(CY(E)),onRaw:l})),tu(I9,{title:y.id?"编辑项目":"新建项目",eyebrow:"PostgreSQL Write Path",loading:n.saving||n.importing},tu("form",{className:"stack-form project-manager-form",onSubmit:Q,"data-testid":"project-manager-form"},y.id?tu("label",null,"项目 ID",tu("input",{value:y.id,disabled:!0})):null,tu("label",null,"序号",tu("input",{type:"number",value:y.sequenceNo,onChange:(E)=>t((D)=>({...D,sequenceNo:E.target.value}))})),tu("label",null,"合同号",tu("input",{value:y.contractNo,onChange:(E)=>t((D)=>({...D,contractNo:E.target.value})),required:!0})),tu("label",null,"项目名称",tu("input",{value:y.name,onChange:(E)=>t((D)=>({...D,name:E.target.value})),required:!0})),tu("label",null,"当前状况",tu("textarea",{value:y.currentStatus,onChange:(E)=>t((D)=>({...D,currentStatus:E.target.value}))})),tu("label",null,"待完成",tu("textarea",{value:y.pending,onChange:(E)=>t((D)=>({...D,pending:E.target.value}))})),tu("label",null,"付款情况",tu("input",{value:y.paymentStatus,onChange:(E)=>t((D)=>({...D,paymentStatus:E.target.value})),placeholder:"例如 1 / 0.5 / 50%"})),tu("label",null,"其它",tu("input",{value:y.notes,onChange:(E)=>t((D)=>({...D,notes:E.target.value}))})),tu("div",{className:"inline-actions"},tu("button",{type:"submit",className:"primary-btn",disabled:n.saving,"data-testid":"project-manager-save-button"},n.saving?"保存中":y.id?"保存修改":"创建项目"),tu("button",{type:"button",className:"ghost-btn",onClick:()=>t({...k9})},"清空"),y.id?tu("button",{type:"button",className:"danger-btn",disabled:n.saving,onClick:w,"data-testid":"project-manager-delete-button"},"删除"):null)),tu("div",{className:"project-manager-import"},tu("p",{className:"muted paragraph"},"浏览器只访问 UniDesk frontend;后端通过同源用户服务代理写入主 PostgreSQL,不暴露 4233 公网端口。"),tu("label",{className:"file-import"},n.importing?"导入中...":"导入 Excel",tu("input",{type:"file",accept:".xlsx",onChange:L,disabled:n.importing,"data-testid":"project-manager-import-input"}))))))}var D6=Pu(Jl(),1);var cu=D6.default.createElement,{useEffect:hY}=D6.default,df=D6.default.useState;function bY({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return cu("span",{className:`status-badge ${f}`},l||u||"unknown")}function B6({label:u,value:l,hint:f,tone:r}){return cu("article",{className:`metric-card ${r||""}`},cu("div",{className:"metric-label"},u),cu("div",{className:"metric-value"},l),cu("div",{className:"metric-hint"},f))}function s9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return cu("section",{className:`panel ${n||""}`},cu("div",{className:"panel-head"},cu("div",null,l?cu("p",{className:"panel-eyebrow"},l):null,cu(nl,{title:u,loading:i})),f?cu("div",{className:"panel-actions"},f):null),cu("div",{className:"panel-body"},r))}function kw({title:u,data:l,onOpen:f,testId:r}){return cu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f(u,l)},"查看原始JSON")}function V6({title:u,text:l}){return cu("div",{className:"empty-state"},cu("strong",null,u),cu("span",null,l))}function vY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function kY(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function IY(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function gw(u){return String(u).replace(/[^a-zA-Z0-9_-]/g,"_")}function gY(u){if(!Number.isFinite(u))return"--";return`${u.toFixed(1)}%`}function qy(u,l){return`${u}/microservices/todo-note/proxy${l}`}function sw(u){return u.reduce((l,f)=>{let r=sw(Array.isArray(f.children)?f.children:[]),n=Boolean(f.completed);return{total:l.total+1+r.total,completed:l.completed+(n?1:0)+r.completed,active:l.active+(n?0:1)+r.active}},{total:0,completed:0,active:0})}function o9(u,l){let f=l==="all"||(l==="completed"?Boolean(u.completed):!u.completed),r=Array.isArray(u.children)?u.children:[];return f||r.some((n)=>o9(n,l))}function Iw(u){return Array.isArray(u?.instances)?u.instances:[]}function a9(u,l){for(let f of u){if(f?.id===l)return Array.isArray(f.children)?f.children:[];let r=a9(Array.isArray(f?.children)?f.children:[],l);if(r.length>0)return r}return[]}function aw({microservices:u,onRaw:l,apiBaseUrl:f="/api"}){let r=u.find((s)=>s.id==="todo-note")||null,[n,i]=df(null),[y,t]=df(null),[_,c]=df(""),[A,j]=df(null),[F,J]=df("all"),[Q,w]=df(13),[L,U]=df(""),[N,q]=df(""),[W,z]=df(""),[Z,H]=df(""),[E,D]=df(""),[h,V]=df(!1),[S,p]=df(""),[O,m]=df(null),X=Iw(y),v=sw(Array.isArray(A?.todos)?A.todos:[]),T=r?vY(r):{},Y=r?IY(r):{},k=r?kY(r):{};async function I(s=_){let[Nu,Eu]=await Promise.all([Tu(`${f}/microservices/todo-note/health`),Tu(qy(f,"/api/instances"))]);i(Nu),t(Eu);let Hu=Iw(Eu),vu=Hu.some((ul)=>ul.id===s)?s:Hu[0]?.id||"";return c(vu),vu}async function b(s=_){if(!s){j(null);return}let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(s)}`));j(Nu)}async function o(s=_){if(!r)return;V(!0),p("");try{let Nu=await I(s);await b(Nu),m(new Date)}catch(Nu){p(Ou(Nu,"Todo Note 加载失败"))}finally{V(!1)}}async function g(s){if(!_)return null;p("");try{let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(_)}/actions`),{method:"POST",body:JSON.stringify({action:s})});return j(Nu),await I(_),Nu}catch(Nu){return p(Ou(Nu,"Todo 操作失败")),null}}async function x(s){s.preventDefault();let Nu=L.trim();if(!Nu)return;V(!0),p("");try{let Eu=await Tu(qy(f,"/api/instances"),{method:"POST",body:JSON.stringify({name:Nu})});U(""),await o(Eu.id)}catch(Eu){p(Ou(Eu,"创建清单失败"))}finally{V(!1)}}async function lu(s){if(!window.confirm("确认删除这个 Todo Note 清单?"))return;V(!0),p("");try{await Tu(qy(f,`/api/instances/${encodeURIComponent(s)}`),{method:"DELETE"}),await o(_===s?"":_)}catch(Nu){p(Ou(Nu,"删除清单失败"))}finally{V(!1)}}async function _u(s){s.preventDefault();let Nu=N.trim();if(!Nu)return;q(""),await g({type:"addTodo",title:Nu})}async function $u(s){if(!_)return;p("");try{let Nu=await Tu(qy(f,`/api/instances/${encodeURIComponent(_)}/${s}`),{method:"POST",body:JSON.stringify({})});j(Nu),await I(_)}catch(Nu){p(Ou(Nu,`${s} 失败`))}}function ju(s){z(s.id),H(String(s.title||""))}async function zu(s){let Nu=Z.trim();if(z(""),H(""),Nu)await g({type:"updateTodoTitle",todoId:s,title:Nu})}async function Wu(s){let Eu=window.prompt("新增子任务标题")?.trim();if(!Eu)return;let Hu=a9(Array.isArray(A?.todos)?A.todos:[],s),vu=new Set(Hu.map((Uf)=>Uf.id)),ul=await g({type:"addTodo",title:Eu,parentId:s,targetIndex:0});if(!ul)return;let mu=a9(Array.isArray(ul?.todos)?ul.todos:[],s),Fl=mu.find((Uf)=>!vu.has(Uf.id));if(Fl&&mu[0]?.id!==Fl.id)await g({type:"moveTodo",todoId:Fl.id,targetParentId:s,targetIndex:0})}async function P(s,Nu){if(!E)return;let Eu={type:"moveTodo",todoId:E,targetIndex:Nu};if(s)Eu.targetParentId=s;D(""),await g(Eu)}if(hY(()=>{o()},[r?.id,r?.runtime?.providerStatus]),!r)return cu(V6,{title:"Todo Note 未登记",text:"请在 config.json 的 microservices 中登记用户服务 id=todo-note"});let e=X.find((s)=>s.id===_)||null,uu=Array.isArray(A?.todos)?A.todos:[],Ku=uu.map((s,Nu)=>({todo:s,index:Nu})).filter((s)=>o9(s.todo,F));return cu("div",{className:"todo-note-page","data-testid":"todo-note-page"},cu(s9,{title:"Todo Note 工作台",eyebrow:"Main Server 用户服务",loading:h,actions:cu("div",{className:"panel-actions"},cu("button",{type:"button",className:"ghost-btn",disabled:h,onClick:()=>o(_),"data-testid":"todo-note-refresh-button"},h?"刷新中":"刷新"),cu(kw,{title:"Todo Note 用户服务",data:r,onOpen:l,testId:"raw-todo-note-service"}))},cu("div",{className:"todo-note-hero"},cu("div",null,cu("div",{className:"node-version-line"},cu(bY,{status:T.providerStatus==="online"?"online":"warn"},T.providerStatus||"unknown"),cu("span",null,r.providerId),cu("span",null,k.public?"公网暴露":"仅 UniDesk frontend 代理访问"),cu("span",null,n?.ok?"Health OK":"Health --")),cu("p",{className:"muted paragraph"},r.description)),cu("div",{className:"microservice-ref-card"},cu("span",null,"Repo"),cu("strong",null,Y.url||"--"),cu("code",null,Y.commitId||"--")),cu("div",{className:"microservice-ref-card"},cu("span",null,"Main Server Docker"),cu("strong",null,`${k.nodeBindHost||"--"}:${k.nodePort||"--"}`),cu("code",null,`${Y.composeService||"--"} / ${Y.containerName||"--"}`))),cu(il,{error:S,wide:!0})),cu("div",{className:"todo-note-layout"},cu(s9,{title:"清单",eyebrow:`${X.length} Instances`,className:"todo-list-panel",loading:h},cu("form",{className:"todo-create-list",onSubmit:x},cu("input",{placeholder:"新清单名称",value:L,onChange:(s)=>U(s.target.value),"aria-label":"新清单名称"}),cu("button",{type:"submit",className:"ghost-btn",disabled:h||!L.trim()},"创建")),X.length===0?cu(V6,{title:"暂无清单",text:"迁移或创建清单后会出现在这里"}):cu("div",{className:"todo-instance-list"},X.map((s)=>cu("button",{key:s.id,type:"button",className:`todo-instance-row ${_===s.id?"active":""}`,onClick:()=>{c(s.id),b(s.id)},"data-testid":`todo-instance-${gw(s.id)}`},cu("strong",null,s.name),cu("span",null,`${s.completedCount??0}/${s.todoCount??0} 完成`),cu("code",null,s.id))))),cu("div",{className:"todo-main-stack"},cu(s9,{title:e?.name||"待选择清单",eyebrow:O?`Updated ${tl(O)}`:"Todo Tree",loading:h,actions:A?cu("div",{className:"panel-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"renameInstance",name:window.prompt("清单新名称",A.name)||A.name})},"重命名"),cu("button",{type:"button",className:"ghost-btn danger",onClick:()=>lu(_)},"删除清单"),cu(kw,{title:`Todo Instance ${_}`,data:A,onOpen:l,testId:"raw-todo-instance"})):null},!A?cu(V6,{title:"未选择清单",text:"左侧选择一个 Todo Note 清单"}):cu("div",{className:"todo-workbench",style:{"--todo-font-size":`${Q}px`}},cu("div",{className:"todo-toolbar"},cu("form",{className:"todo-add-form",onSubmit:_u},cu("input",{placeholder:"新增根任务",value:N,onChange:(s)=>q(s.target.value),"aria-label":"新增根任务"}),cu("button",{type:"submit",className:"ghost-btn",disabled:!N.trim()},"新增")),cu("div",{className:"todo-filter-strip"},["all","active","completed"].map((s)=>cu("button",{key:s,type:"button",className:`todo-filter ${F===s?"active":""}`,onClick:()=>J(s)},s==="all"?"全部":s==="active"?"未完成":"已完成"))),cu("div",{className:"todo-toolbar-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"setAllTodosExpanded",expanded:!0})},"全部展开"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>g({type:"setAllTodosExpanded",expanded:!1})},"全部收起"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>$u("undo")},"撤销"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>$u("redo")},"重做"),cu("label",{className:"todo-font-control"},"字号",cu("input",{type:"range",min:11,max:18,value:Q,onChange:(s)=>w(Number(s.target.value))})))),cu("div",{className:"todo-stats-grid"},cu(B6,{label:"总任务",value:v.total,hint:`${X.length} lists`}),cu(B6,{label:"已完成",value:v.completed,hint:`${gY(v.total?v.completed/v.total*100:0)}`,tone:"ok"}),cu(B6,{label:"未完成",value:v.active,hint:F==="active"?"当前筛选":"active tasks",tone:v.active>0?"warn":"ok"}),cu(B6,{label:"历史指针",value:A.historyPointer??0,hint:"undo / redo"})),cu("div",{className:"todo-root-drop",onDragOver:(s)=>s.preventDefault(),onDrop:(s)=>{s.preventDefault(),P(null,uu.length)}},"拖到这里可移为根任务末尾"),cu("div",{className:"todo-tree","data-testid":"todo-note-tree"},Ku.length===0?cu(V6,{title:"没有匹配任务",text:"调整筛选或新增任务"}):Ku.map(({todo:s,index:Nu})=>cu(ow,{key:s.id,todo:s,depth:0,parentId:null,index:Nu,siblingCount:uu.length,filter:F,editingId:W,editingTitle:Z,setEditingTitle:H,beginEdit:ju,saveEdit:zu,applyTodoAction:g,addChild:Wu,dragTodoId:E,setDragTodoId:D,dropTodo:P}))))))))}function ow(u){let{todo:l,depth:f,parentId:r,index:n,siblingCount:i,filter:y,editingId:t,editingTitle:_,setEditingTitle:c,beginEdit:A,saveEdit:j,applyTodoAction:F,addChild:J,dragTodoId:Q,setDragTodoId:w,dropTodo:L}=u,U=Array.isArray(l.children)?l.children:[],N=U.map((z,Z)=>({child:z,childIndex:Z})).filter((z)=>o9(z.child,y)),q=t===l.id,W=r||null;return cu("div",{className:"todo-row-wrap"},cu("article",{className:`todo-row ${l.completed?"completed":""} ${Q===l.id?"dragging":""}`,style:{"--todo-depth":f},draggable:!0,onDragStart:(z)=>{w(l.id),z.dataTransfer.effectAllowed="move"},onDragOver:(z)=>z.preventDefault(),onDrop:(z)=>{z.preventDefault(),L(l.id,U.length)},"data-testid":`todo-row-${gw(l.id)}`},cu("button",{type:"button",className:"todo-expand",disabled:U.length===0,onClick:()=>F({type:"toggleTodoExpanded",todoId:l.id})},U.length===0?"·":l.expanded?"▾":"▸"),cu("input",{type:"checkbox",checked:Boolean(l.completed),onChange:()=>F({type:"toggleTodoCompleted",todoId:l.id}),"aria-label":`完成 ${l.title}`}),cu("div",{className:"todo-title-cell",onDoubleClick:()=>A(l)},q?cu("div",{className:"todo-edit-inline"},cu("input",{value:_,autoFocus:!0,onChange:(z)=>c(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")j(l.id);if(z.key==="Escape")A({id:"",title:""})}}),cu("button",{type:"button",className:"ghost-btn",onClick:()=>j(l.id)},"保存")):cu("strong",null,l.title||"Untitled"),cu("div",{className:"todo-meta-line"},cu("span",null,`子项 ${U.length}`),cu("span",null,`更新 ${qu(l.updatedAt)}`),l.reminderAt?cu("span",{className:"todo-reminder"},`提醒 ${qu(l.reminderAt)}`):cu("span",null,"无提醒"))),cu("input",{className:"todo-reminder-input",type:"datetime-local",value:r8(l.reminderAt),onChange:(z)=>F({type:"setTodoReminder",todoId:l.id,reminderAt:h7(z.target.value)})}),cu("div",{className:"todo-row-actions"},cu("button",{type:"button",className:"ghost-btn",onClick:()=>A(l)},"编辑"),cu("button",{type:"button",className:"ghost-btn",onClick:()=>J(l.id)},"子项"),cu("button",{type:"button",className:"ghost-btn",disabled:n<=0,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:n-1})},"上移"),cu("button",{type:"button",className:"ghost-btn",disabled:n<=0,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:0})},"置顶"),cu("button",{type:"button",className:"ghost-btn",disabled:n>=i-1,onClick:()=>F({type:"moveTodo",todoId:l.id,...W?{targetParentId:W}:{},targetIndex:n+1})},"下移"),cu("button",{type:"button",className:"ghost-btn",disabled:!r,onClick:()=>F({type:"moveTodo",todoId:l.id,targetIndex:9999})},"提升"),cu("button",{type:"button",className:"ghost-btn danger",onClick:()=>F({type:"deleteTodo",todoId:l.id})},"删除"))),l.expanded&&N.length>0?cu("div",{className:"todo-children"},N.map(({child:z,childIndex:Z})=>cu(ow,{key:z.id,todo:z,depth:f+1,parentId:l.id,index:Z,siblingCount:U.length,filter:y,editingId:t,editingTitle:_,setEditingTitle:c,beginEdit:A,saveEdit:j,applyTodoAction:F,addChild:J,dragTodoId:Q,setDragTodoId:w,dropTodo:L}))):null)}var dw=Pu(Jl(),1),kn=dw.default.createElement;function ew({title:u,items:l,actions:f,className:r,testId:n}){let i=Array.isArray(l)?l:[];return kn("section",{className:`top-status-bar ${r||""}`,"data-testid":n},kn("div",{className:"top-status-main"},u?kn("strong",{className:"top-status-title"},u):null,kn("div",{className:"top-status-chips"},i.map((y,t)=>kn("span",{key:y?.key||`${y?.label||"status"}-${t}`,className:`top-status-chip ${y?.tone||""}`,"data-testid":y?.testId},y?.label?kn("b",null,y.label):null,kn("span",null,y?.value??"--"))))),f?kn("div",{className:"top-status-actions"},f):null)}var M_=Pu(Jl(),1);var Qu=M_.default.createElement,{useEffect:sY,useMemo:aY}=M_.default,oY=M_.default.useState;function uL({status:u,children:l,title:f}){let r=String(u||"unknown").toLowerCase();return Qu("span",{className:`status-badge ${r}`,title:f},l||u||"unknown")}function X6({label:u,value:l,hint:f,tone:r}){return Qu("article",{className:`metric-card ${r||""}`},Qu("div",{className:"metric-label"},u),Qu("div",{className:"metric-value"},l),Qu("div",{className:"metric-hint"},f))}function d9({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){return Qu("section",{className:`panel ${n||""}`},Qu("div",{className:"panel-head"},Qu("div",null,l?Qu("p",{className:"panel-eyebrow"},l):null,Qu(nl,{title:u,loading:i})),f?Qu("div",{className:"panel-actions"},f):null),Qu("div",{className:"panel-body"},r))}function lL({title:u,data:l,onOpen:f,testId:r}){return Qu("button",{type:"button",className:"ghost-btn","data-testid":r,onClick:()=>f?.(u,l)},"查看原始JSON")}function e9({title:u,text:l}){return Qu("div",{className:"empty-state"},Qu("strong",null,u),Qu("span",null,l))}function Wy(u){return Array.isArray(u)?u:[]}function u7(u){return u&&typeof u==="object"&&!Array.isArray(u)?u:{}}function dY(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function eY(u,l){return`${u}/microservices/k3sctl-adapter/proxy${l}`}function up(u){return u.find((l)=>String(l?.id||"")==="k3sctl-adapter")||null}function lp(u){if(u?.healthy===!0)return"online";if(String(u?.role||"")==="standby")return"warn";return"failed"}function fp(u){return u?.healthy===!0?"online":"failed"}function rp(u){if(u===!0)return"YES";if(u===!1)return"NO";return"--"}function np(u){return Array.from(new Set(u.flatMap((l)=>Wy(l?.expectedNodeIds).map((f)=>String(f))))).filter(Boolean).sort()}function ip(u){let l=u.find((f)=>f?.id==="code-queue")||u[0];return String(l?.activeInstanceId||"--")}function yp(u){return Qu("article",{key:u?.id||u?.nodeId,className:"k3s-instance-card"},Qu("div",{className:"node-card-head"},Qu("strong",null,u?.nodeId||u?.id||"--"),Qu(uL,{status:lp(u)},u?.healthy?"HEALTHY":"DEGRADED")),Qu("div",{className:"k3s-instance-role"},Qu("span",null,String(u?.role||"worker").toUpperCase()),Qu("code",null,u?.id||"--")),Qu("dl",{className:"k3s-kv"},Qu("dt",null,"Base URL"),Qu("dd",null,Qu("code",null,u?.baseUrl||"--")),Qu("dt",null,"Proxy"),Qu("dd",null,u?.proxyMode||"--"),Qu("dt",null,"Health"),Qu("dd",null,`${u?.upstreamStatus??"--"} / ${u?.status||"unknown"}`),Qu("dt",null,"Checked"),Qu("dd",null,qu(u?.checkedAt))))}function tp(u,l){let f=Wy(u?.instances),r=u7(u?.active);return Qu(d9,{key:u?.id||"service",title:u?.id||"managed-service",eyebrow:`${u?.namespace||"unidesk"} / k3s managed service`,className:"k3s-service-panel",actions:Qu(lL,{title:`k3s service ${u?.id||""}`,data:u,onOpen:l,testId:`raw-k3s-service-${u?.id||"unknown"}`})},Qu("div",{className:"k3s-service-summary"},Qu("div",null,Qu("span",null,"状态"),Qu(uL,{status:fp(u)},u?.status||"unknown")),Qu("div",null,Qu("span",null,"Active"),Qu("strong",null,u?.activeInstanceId||"--")),Qu("div",null,Qu("span",null,"Single Writer"),Qu("strong",null,rp(u?.singleWriter))),Qu("div",null,Qu("span",null,"Active Health"),Qu("strong",null,r?.upstreamStatus??"--"))),f.length===0?Qu(e9,{title:"暂无 k3s 实例",text:"adapter 没有返回该服务的 endpoint 列表"}):Qu("div",{className:"k3s-instance-grid"},f.map(yp)))}function fL({microservices:u,onRaw:l,apiBaseUrl:f,onNavigate:r}){let n=up(Array.isArray(u)?u:[]),i=dY(n),[y,t]=oY({loading:!1,error:"",data:null,refreshedAt:null});async function _(){t((N)=>({...N,loading:!0,error:""}));try{let N=await Tu(eY(f,"/api/control-plane"));t({loading:!1,error:"",data:N,refreshedAt:new Date})}catch(N){t((q)=>({...q,loading:!1,error:Ou(N,"加载 k3s 控制平面失败")}))}}sY(()=>{_()},[f]);let c=aY(()=>Wy(y.data?.services),[y.data]),A=np(c),j=c.filter((N)=>N?.healthy===!0).length,F=c.reduce((N,q)=>N+Wy(q?.instances).length,0),J=c.reduce((N,q)=>N+Wy(q?.instances).filter((W)=>W?.healthy===!0).length,0),Q=ip(c),w=u7(y.data?.kubectl),L=u7(y.data?.kubeApiProxy),U=Wy(y.data?.manifestPaths).map((N)=>String(N));if(!n)return Qu(e9,{title:"k3sctl-adapter 未登记",text:"请在 config.json 的 microservices 中登记 id=k3sctl-adapter,并通过该微服务连接 k3s 控制平面。"});return Qu("div",{className:"k3s-page","data-testid":"k3sctl-page"},Qu(d9,{title:"K3S Control Plane",eyebrow:"Managed by k3sctl-adapter",className:"k3s-hero-panel",loading:y.loading,actions:Qu(M_.default.Fragment,null,Qu("button",{type:"button",className:"ghost-btn",onClick:_,disabled:y.loading,"data-testid":"k3s-refresh-button"},y.loading?"刷新中":"刷新"),r?Qu("button",{type:"button",className:"ghost-btn",onClick:()=>r("apps","code-queue"),"data-testid":"k3s-open-code-queue"},"打开 Code Queue"):null,Qu(lL,{title:"k3sctl-adapter microservice",data:n,onOpen:l,testId:"raw-k3s-adapter"}))},Qu("div",{className:"k3s-hero"},Qu("div",{className:"k3s-orb","aria-hidden":"true"},Qu("span",null,"k3s")),Qu("div",{className:"k3s-hero-copy"},Qu("p",{className:"eyebrow"},"D601 control plane / D518 managed node"),Qu("h2",null,"UniDesk 只管理 adapter;业务微服务交给 k3s 标准服务路由"),Qu("p",{className:"muted paragraph"},"Code Queue 的前端/API 请求进入 k3sctl-adapter,再由 adapter 转发到 k3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"),Qu("div",{className:"k3s-route-strip"},Qu("span",null,"NO FALLBACK"),Qu("code",null,y.data?.runtimePath||"frontend -> backend-core -> k3sctl-adapter")))),Qu("div",{className:"metric-grid"},Qu(X6,{label:"控制面",value:y.data?.clusterId||"D601",hint:`adapter ${i.providerStatus||"unknown"}`,tone:i.providerStatus==="online"?"ok":"warn"}),Qu(X6,{label:"代管服务",value:c.length,hint:`${j}/${c.length||0} healthy`,tone:j===c.length&&c.length>0?"ok":"warn"}),Qu(X6,{label:"节点",value:A.join(" / ")||"--",hint:"expected k3s nodes"}),Qu(X6,{label:"实例",value:`${J}/${F}`,hint:`active ${Q}`,tone:J===F&&F>0?"ok":"warn"})),Qu("div",{className:"k3s-control-plane-grid"},Qu("article",{className:"k3s-control-plane-card"},Qu("span",null,"service proxy"),Qu("strong",null,L.configured===!0?"K8S API PROXY":"PROXY DEGRADED"),Qu("p",null,L.configured===!0?`${L.mode||"kubernetes-api-service-proxy"} via ${L.connectHost||"--"}`:"adapter 必须通过 k8s API service proxy 访问业务服务,不回退到业务容器直连。")),Qu("article",{className:"k3s-control-plane-card"},Qu("span",null,"manifests"),Qu("strong",null,U.length||"--"),Qu("p",null,U.join(" / ")||"未配置 manifest")),Qu("article",{className:"k3s-control-plane-card"},Qu("span",null,"cluster snapshot"),Qu("strong",null,w.enabled===!0?w.ok===!0?"KUBECTL OK":"KUBECTL DEGRADED":"API ONLY"),Qu("p",null,w.enabled===!0?`nodes ${w.nodeCount??"--"}`:"控制面页面以 adapter 返回的 k8s service proxy 状态为准;kubectl 只作为可选快照。"))),y.error?Qu(il,{error:y.error}):null,y.refreshedAt?Qu("p",{className:"muted paragraph"},`最近刷新 ${tl(y.refreshedAt)}`):null),c.length===0?Qu(d9,{title:"代管服务",eyebrow:"k3s services",loading:y.loading},Qu(e9,{title:"暂无 k3s 服务",text:"等待 k3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。"})):c.map((N)=>tp(N,l)))}var R_=Pu(Jl(),1);var hl=R_.default.createElement;function rL({onClose:u}){let{notifications:l,removeNotification:f,clearNotifications:r}=Xf(),n=R_.default.useRef(null);if(R_.default.useEffect(()=>{let i=(y)=>{if(n.current&&!n.current.contains(y.target))u()};return document.addEventListener("mousedown",i),()=>document.removeEventListener("mousedown",i)},[u]),l.length===0)return hl("div",{className:"notification-popup",ref:n},hl("div",{className:"notification-popup-header"},hl("span",null,"通知"),hl("button",{className:"notification-popup-close",onClick:u},"×")),hl("div",{className:"notification-popup-empty"},"暂无通知"));return hl("div",{className:"notification-popup",ref:n},hl("div",{className:"notification-popup-header"},hl("span",null,`通知 (${l.length})`),hl("div",{className:"notification-popup-actions"},hl("button",{className:"notification-popup-clear",onClick:r},"清空"),hl("button",{className:"notification-popup-close",onClick:u},"×"))),hl("div",{className:"notification-popup-list"},l.slice().reverse().map((i)=>hl("div",{key:i.id,className:`notification-item ${i.type}`},hl("span",{className:"notification-item-icon"},i.type==="success"?"✓":"×"),hl("span",{className:"notification-item-message"},i.message),hl("button",{className:"notification-item-dismiss",onClick:()=>f(i.id)},"×")))))}function nL({notification:u}){let{removeNotification:l}=Xf();return R_.default.useEffect(()=>{let f=setTimeout(()=>{l(u.id)},3000);return()=>clearTimeout(f)},[u.id,l]),hl("div",{className:`notification-banner ${u.type}`,role:"alert"},hl("span",{className:"notification-banner-icon"},u.type==="success"?"✓":"×"),hl("span",{className:"notification-banner-message"},u.message),hl("button",{className:"notification-banner-dismiss",onClick:()=>l(u.id)},"×"))}function QL(u,l){let f=document.getElementById("root")?.getAttribute(u);if(!f)return l;try{let r=JSON.parse(f);return typeof r==="object"&&r!==null&&!Array.isArray(r)?r:l}catch{return l}}var gu=QL("data-config",{apiBaseUrl:"/api",authUsername:"admin"}),_p=QL("data-codex-overview",null),$=gn.default.createElement,{useEffect:J0,useMemo:b_}=gn.default,bu=gn.default.useState,n7=gn.default.createContext(!1),Dr=oQ(ic),$p={id:"code-queue",name:"Code Queue",providerId:"D601",description:"Code Queue",repository:{containerName:"k3s:code-queue"},backend:{nodeBaseUrl:"k3s://code-queue",nodeBindHost:"k3s://unidesk/code-queue",nodePort:4222,proxyMode:"k3sctl-adapter-http",public:!1},deployment:{mode:"k3sctl-managed",adapterServiceId:"k3sctl-adapter",k3sServiceId:"code-queue"},runtime:{orchestrator:"k3sctl",providerStatus:"loading",providerName:"D601"}};function iL(){return typeof document>"u"||document.visibilityState!=="hidden"}function cp(u,l){if(u==="ops"&&l==="status")return 5000;if(u==="nodes"&&l==="monitor")return 5000;if(u==="tasks"&&(l==="dispatch"||l==="scheduled"||l==="pending"))return 5000;if(u==="nodes"||u==="ops")return 1e4;if(u==="apps")return 15000;if(u==="tasks")return 15000;return 30000}async function Ap(u){if(!u?._summaryOnly||!u?.id)return u;return(await Tu(`${gu.apiBaseUrl}/tasks/${encodeURIComponent(String(u.id))}`))?.task||u}function v_(u){return u?._summaryOnly?{...u,_loadRaw:()=>Ap(u)}:u}function b0(u){if(!Number.isFinite(u))return"--";let l=Math.max(0,u);if(l===0)return"0s";if(l<0.01)return"<0.01s";if(l<0.1)return`${l.toFixed(2)}s`;if(l<1)return`${l.toFixed(1)}s`;if(l<10&&!Number.isInteger(l))return`${l.toFixed(1)}s`;if(l<60)return`${Math.round(l)}s`;let f=Math.floor(l);if(f<3600)return`${Math.floor(f/60)}m ${f%60}s`;return`${Math.floor(f/3600)}h ${Math.floor(f%3600/60)}m`}function Nr(u){let l=Number(u);if(!Number.isFinite(l))return"--";if(l<1)return`${Math.max(0,l).toFixed(1)}ms`;if(l<10)return`${l.toFixed(1)}ms`;if(l<1000)return`${Math.round(l)}ms`;return b0(l/1000)}function Tf(u){let l=Number(u);if(!Number.isFinite(l)||l<=0)return"--";let f=["B","KB","MB","GB","TB"],r=l,n=0;while(r>=1024&&n0)return f[r]}return"任务失败但 provider 未返回明确原因"}function hi(u){if(u===null||u===void 0)return"--";if(typeof u==="boolean")return u?"是":"否";if(typeof u==="number")return String(u);if(typeof u==="string")return u.length>80?`${u.slice(0,77)}...`:u;if(Array.isArray(u))return`${u.length} 项`;if(typeof u==="object")return`${Object.keys(u).length} 字段`;return String(u)}function Fp(u,l){let f=u.replace(/[-_\s]/g,"").toLowerCase(),r=f==="ts"||f.endsWith("at")||f.endsWith("timestamp")||f.endsWith("heartbeat");if((typeof l==="string"||typeof l==="number")&&r){let n=qu(l);if(n!=="--")return n}if(u==="bodyText"&&typeof l==="string")return`${/^\s*[{[]/.test(l)?"JSON":"HTTP"} body ${l.length} chars`;return hi(l)}function WL(u){if(!u||typeof u!=="object"||Array.isArray(u))return[];return Object.entries(u)}function ef(u){return String(u).replace(/[^a-zA-Z0-9_-]/g,"_")}function y7(u,l){return u&&typeof u==="object"&&!Array.isArray(u)?u[l]:void 0}function Y6(u,l,f="未知"){let r=y7(u?.labels,l);return typeof r==="string"&&r.length>0?r:f}function wL(u){return Y6(u,"providerGatewayVersion")}function h_(u){return Y6(u,"providerGatewayUpgradePolicy")}function yL(u){return Y6(u,"providerGatewayStartedAt","")}function LL(u){let l=y7(u?.labels,"unideskCapabilities");if(typeof l==="string")return l.split(",").map((f)=>f.trim()).filter(Boolean);return Array.isArray(l)?l.filter((f)=>typeof f==="string"):[]}function KL(u,l){return LL(u).includes(l)}function tL(u,l){let f=y7(u?.labels,l);return f===!0||f==="true"||f==="1"}function Up(u){if(!KL(u,"host.ssh"))return{tone:"fail",label:"不可用",detail:"未声明 host.ssh"};if(!tL(u,"hostSshConfigured"))return{tone:"warn",label:"未配置",detail:"缺少 SSH 环境变量"};if(!tL(u,"hostSshKeyPresent"))return{tone:"warn",label:"缺 key",detail:"私钥未挂载"};return{tone:"ok",label:"可用",detail:Y6(u,"hostSshTarget","host.ssh ready")}}function Jp(u){if(!KL(u,"provider.upgrade"))return{tone:"fail",label:"不可用",detail:"未声明 provider.upgrade"};let l=h_(u);if(l!=="always-enabled")return{tone:"warn",label:"待确认",detail:`策略 ${l}`};return{tone:"ok",label:"可用",detail:"always-enabled"}}function t7(u){let l=typeof u==="string"&&u.length>0?u:"未知";if(l==="未知")return"版本未知";return l.startsWith("v")?l:`v${l}`}function GL(u){return u?.payload&&typeof u.payload==="object"&&!Array.isArray(u.payload)?u.payload:{}}function p6(u){return u?.result&&typeof u.result==="object"&&!Array.isArray(u.result)?u.result:{}}function S6(u){let l=GL(u),f=p6(u);return(l.mode??f.mode)==="schedule"?"schedule":"plan"}function Qp(u){let l=GL(u).source;return typeof l==="string"&&l.length>0?l:"unknown"}function Np(u){let l=p6(u),f=l.plan&&typeof l.plan==="object"&&!Array.isArray(l.plan)?l.plan:{},r=l.policy??f.policy;return typeof r==="string"&&r.length>0?r:"--"}function zL(u){let l=p6(u),f=l.plan&&typeof l.plan==="object"&&!Array.isArray(l.plan)?l.plan:{},r=l.targetProviderGatewayVersion??l.providerGatewayVersion??f.targetProviderGatewayVersion??f.providerGatewayVersion;return typeof r==="string"&&r.length>0?t7(r):"版本未知"}function TL(u){if(String(u?.status||"").toLowerCase()==="failed")return qL(u);if(Ly(u))return"等待 provider 回传升级终态";let f=p6(u);if(typeof f.updaterContainerId==="string"&&f.updaterContainerId.length>0)return`updater ${f.updaterContainerId.slice(0,18)}`;if(typeof f.message==="string"&&f.message.length>0)return f.message;if(f.plan)return"升级计划已生成";return"无升级结果摘要"}function EL(u,l){return u.filter((f)=>f?.providerId===l&&f?.command==="provider.upgrade").sort((f,r)=>(Q0(r.updatedAt)??0)-(Q0(f.updatedAt)??0))}function qp(u){return u.find((l)=>S6(l)==="schedule")||u[0]||null}function ZL(u){return u?.runtime&&typeof u.runtime==="object"&&!Array.isArray(u.runtime)?u.runtime:{}}function _L(u){return u?.backend&&typeof u.backend==="object"&&!Array.isArray(u.backend)?u.backend:{}}function Wp(u){return u?.repository&&typeof u.repository==="object"&&!Array.isArray(u.repository)?u.repository:{}}function wl({status:u,children:l}){let f=String(u||"unknown").toLowerCase();return $("span",{className:`status-badge ${f}`},l||u||"unknown")}function cl({label:u,value:l,hint:f,tone:r,onClick:n,testId:i}){let y=typeof n==="function";return $("article",{className:`metric-card ${r||""} ${y?"clickable":""}`,role:y?"button":void 0,tabIndex:y?0:void 0,"data-testid":i,onClick:n,onKeyDown:y?(t)=>{if(t.key==="Enter"||t.key===" ")t.preventDefault(),n()}:void 0},$("div",{className:"metric-label"},u),$("div",{className:"metric-value"},l),$("div",{className:"metric-hint"},f))}function du({title:u,eyebrow:l,actions:f,children:r,className:n,loading:i}){let y=gn.default.useContext(n7),t=Boolean(i)||y;return $("section",{className:`panel ${n||""}`},$("div",{className:"panel-head"},$("div",null,l?$("p",{className:"panel-eyebrow"},l):null,$(nl,{title:u,loading:t})),f?$("div",{className:"panel-actions"},f):null),$("div",{className:"panel-body"},r))}function bl({title:u,data:l,onOpen:f,testId:r}){let[n,i]=bu(!1),y=l&&typeof l==="object"&&typeof l._loadRaw==="function"?l._loadRaw:null;async function t(){if(!y){f(u,l);return}i(!0);try{f(u,await y())}catch(_){f(u,{ok:!1,error:Ou(_,"读取原始 JSON 失败"),fallback:l})}finally{i(!1)}}return $("button",{type:"button",className:"ghost-btn","data-testid":r,disabled:n,onClick:()=>void t()},n?"读取中":"查看原始JSON")}function wp({raw:u,onClose:l}){if(!u)return null;return $("div",{className:"modal-backdrop",role:"presentation"},$("section",{className:"raw-dialog",role:"dialog","aria-modal":"true","aria-label":u.title},$("div",{className:"raw-dialog-head"},$("h2",null,u.title),$("button",{type:"button",className:"ghost-btn",onClick:l},"关闭")),$("pre",{className:"raw-json","data-testid":"raw-json"},JSON.stringify(u.data,null,2))))}function OL({labels:u,limit:l=8}){let f=WL(u).slice(0,l);if(f.length===0)return $("span",{className:"muted"},"无标签");return $("div",{className:"chip-row"},f.map(([r,n])=>$("span",{key:r,className:"data-chip"},$("b",null,r),$("span",null,hi(n)))))}function wy({node:u}){let l=wL(u);return $("span",{className:`version-chip ${l==="未知"?"unknown":""}`,"data-testid":`gateway-version-${ef(u?.providerId||"unknown")}`},t7(l))}function $L({title:u,state:l,testId:f}){return $("span",{className:`capability-badge ${l.tone}`,title:l.detail,"data-testid":f},$("b",null,u),$("strong",null,l.label),$("small",null,l.detail))}function _7({node:u}){let l=ef(u?.providerId||"unknown");return $("div",{className:"node-availability-strip"},$($L,{title:"SSH 透传",state:Up(u),testId:`ssh-availability-${l}`}),$($L,{title:"远程更新",state:Jp(u),testId:`upgrade-availability-${l}`}))}function sn({data:u,empty:l="无数据"}){if(u===null||u===void 0)return $("span",{className:"muted"},l);if(typeof u!=="object")return $("span",{className:"summary-value"},hi(u));if(Array.isArray(u))return $("span",{className:"summary-value"},`${u.length} 项列表`);let f=Object.entries(u).slice(0,5);if(f.length===0)return $("span",{className:"muted"},l);return $("div",{className:"summary-grid"},f.map(([r,n])=>$("span",{key:r,className:"summary-item"},$("b",null,r),$("span",null,Fp(r,n)))))}function jl({title:u,text:l}){return $("div",{className:"empty-state"},$("strong",null,u),$("span",null,l))}function Lp({onLogin:u}){let[l,f]=bu(gu.authUsername||"admin"),[r,n]=bu(""),[i,y]=bu(""),[t,_]=bu(!1);async function c(A){A.preventDefault(),_(!0),y("");try{let j=await Tu("/login",{method:"POST",body:JSON.stringify({username:l,password:r})});u(j)}catch(j){y(Ou(j,"登录失败"))}finally{_(!1)}}return $("main",{className:"login-screen","data-testid":"login-screen"},$("section",{className:"login-card"},$("div",{className:"login-brand"},$("span",{className:"brand-mark"},"UD"),$("div",null,$("h1",null,"UniDesk"),$("p",null,"Control Plane Login"))),$("form",{className:"login-form",onSubmit:c},$("label",null,"账号",$("input",{name:"username",autoComplete:"username",value:l,onChange:(A)=>f(A.target.value)})),$("label",null,"密码",$("input",{name:"password",type:"password",autoComplete:"current-password",value:r,onChange:(A)=>n(A.target.value)})),$(il,{error:i}),$("button",{type:"submit",disabled:t},t?"登录中":"登录")),$("div",{className:"login-note"},"默认账号由 config.json 注入;公网入口只暴露前端登录面。")))}function Kp({connection:u,lastRefresh:l,onRefresh:f,onLogout:r,session:n,clock:i,activeStatusItems:y=[],onNotificationToggle:t,unreadCount:_=0}){let c=[{key:"core",label:"核心",value:u.text,tone:u.ok?"ok":"fail",testId:"conn-text"},...Array.isArray(y)?y:[],{key:"refresh",label:"刷新",value:l?tl(l):"未刷新"},{key:"clock",label:c$,value:tl(i)},{key:"user",label:"用户",value:n?.user?.username||"--",tone:"user"}];return $("header",{className:"topbar"},$("div",null,$("p",{className:"eyebrow"},"Distributed Work Platform"),$("h1",null,"UniDesk 控制平面")),$(ew,{className:"global-top-status",title:"状态",items:c,actions:[$("button",{key:"notification",type:"button",className:`notification-icon-btn ${_>0?"has-unread":""}`,onClick:t,"aria-label":"通知"},"\uD83D\uDD14",_>0?$("span",{key:"badge",className:"notification-badge"},_>99?"99+":_):null),$("button",{key:"refresh",type:"button",className:"ghost-btn",onClick:f},"刷新"),$("button",{key:"logout",type:"button",className:"ghost-btn danger",onClick:r},"退出")]}))}function Gp(u){return!u.defaultPrevented&&u.button===0&&!u.metaKey&&!u.altKey&&!u.ctrlKey&&!u.shiftKey&&u.currentTarget.target!=="_blank"}function HL({moduleId:u,tabId:l,className:f,active:r=!1,title:n,testId:i,onNavigate:y,children:t}){let _=yc(Dr,u,l);return $("a",{href:_,role:"button",className:f,title:n,"aria-current":r?"page":void 0,"data-testid":i,"data-route":_,onClick:(c)=>{if(!Gp(c))return;c.preventDefault(),y(u,l)}},t)}function zp({activeModule:u,activeTabs:l,onNavigate:f,collapsed:r,onToggle:n}){return $("aside",{className:`rail ${r?"collapsed":""}`,"aria-label":"主模块"},$("div",{className:"brand"},$("span",{className:"brand-mark"},"UD"),$("span",{className:"brand-text"},"UniDesk"),$("button",{type:"button",className:"rail-toggle",onClick:n,"aria-label":r?"展开左侧边栏":"收起左侧边栏","data-testid":"rail-toggle"},r?"»":"«")),ic.map((i)=>{let y=l[i.id]||st[i.id]||i.tabs[0]?.id||"";return $(HL,{key:i.id,moduleId:i.id,tabId:y,className:`module ${u===i.id?"active":""}`,active:u===i.id,title:i.label,onNavigate:f},$("span",{className:"module-code"},i.code),$("span",null,i.label))}))}function Tp({module:u,activeTab:l,onNavigate:f}){return $("nav",{className:"tabs","aria-label":`${u.label} 子功能`},u.tabs.map((r)=>$(HL,{key:r.id,moduleId:u.id,tabId:r.id,className:`tab ${l===r.id?"active":""}`,active:l===r.id,onNavigate:f},r.label)))}function Ep({data:u,onRaw:l,onNavigate:f}){let r=u.overview||{},n=u.nodes.filter((J)=>J.status==="online"),i=u.pendingTasks||u.tasks.filter(Ly),y=r.pendingTaskCount??i.length,t=u.tasks.slice(0,5),_=r.pgdata||{},c=r.microserviceAvailability||{},A=Mu(c.totalCount),j=Mu(c.healthyCount),F=Mu(c.unhealthyCount);return $("div",{className:"page-grid overview-grid","data-testid":"overview-page"},$(du,{title:"核心指标",eyebrow:"Control"},$("div",{className:"metric-grid"},$(cl,{label:"数据库",value:r.dbReady?"READY":"WAIT",hint:"PostgreSQL internal network",tone:r.dbReady?"ok":"warn"}),$(cl,{label:"PGDATA",value:Tf(_.databaseBytes),hint:`${_.volumeName||"unidesk_pgdata_10gb"} / ${_.databasePretty||"--"}`,tone:"ok",testId:"pgdata-usage-card"}),$(cl,{label:"在线节点",value:r.onlineNodeCount??0,hint:`${r.nodeCount??0} registered`,tone:"ok"}),$(cl,{label:"WebSocket",value:r.activeSocketCount??0,hint:"Provider ingress sockets"}),$(cl,{label:"用户服务可用",value:A>0?`${j}/${A}`:"--",hint:A>0?`healthyCount ${j} · unhealthyCount ${F}`:"strict /health probes",tone:A>0&&F===0?"ok":"warn",testId:"microservice-availability-card"}),$(cl,{label:"待处理任务",value:y,hint:y>0?"点击查看具体任务":`timeout ${b0(Math.floor((r.taskPendingTimeoutMs??0)/1000))}`,tone:y>0?"warn":"ok",onClick:()=>f("tasks","pending"),testId:"pending-task-card"}))),$(du,{title:"本机 Provider",eyebrow:"Self Connected"},n.length===0?$(jl,{title:"暂无在线节点",text:"provider-gateway 未完成自接入"}):$("div",{className:"node-card-list"},n.slice(0,4).map((J)=>$(Zp,{key:J.providerId,node:J,onRaw:l})))),$(du,{title:"待处理任务明细",eyebrow:`${y} Pending`,actions:$("button",{type:"button",className:"ghost-btn",onClick:()=>f("tasks","pending"),"data-testid":"pending-task-detail-link"},"进入任务调度")},i.length===0?$(jl,{title:"当前无待处理",text:"queued / dispatched / running 超时后会自动转为 failed,避免总览长期卡住"}):$("div",{className:"compact-list"},i.slice(0,5).map((J)=>$(FL,{key:J.id,task:J,onRaw:l})))),$(du,{title:"最近任务",eyebrow:"Dispatch"},t.length===0?$(jl,{title:"暂无任务",text:"可以在任务调度模块发起 docker.ps 或 echo"}):$("div",{className:"compact-list"},t.map((J)=>$(FL,{key:J.id,task:J,onRaw:l})))))}function Zp({node:u,onRaw:l}){return $("article",{className:"node-card"},$("div",{className:"node-card-head"},$("div",null,$("strong",null,u.name),$("code",null,u.providerId)),$(wl,{status:u.status})),$("div",{className:"node-version-line"},$(wy,{node:u}),$("span",null,`升级策略 ${h_(u)}`)),$(_7,{node:u}),$(OL,{labels:u.labels,limit:6}),$("div",{className:"node-card-foot"},$("span",null,`心跳 ${qu(u.lastHeartbeat)}`),$(bl,{title:`Provider ${u.providerId}`,data:u,onOpen:l,testId:`raw-node-${ef(u.providerId)}`})))}function Op({events:u,onRaw:l}){return $(du,{title:"事件摘要",eyebrow:"Latest 100"},u.length===0?$(jl,{title:"暂无事件",text:"Provider 注册、心跳超时和任务状态会写入事件流"}):$("div",{className:"table-wrap"},$("table",null,$("thead",null,$("tr",null,$("th",null,"ID"),$("th",null,"类型"),$("th",null,"来源"),$("th",null,"摘要"),$("th",null,"时间"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.id},$("td",null,$("code",null,f.id)),$("td",null,$(wl,{status:f.type},f.type)),$("td",null,$("code",null,f.source)),$("td",null,$(sn,{data:f.payload})),$("td",null,qu(f.createdAt)),$("td",null,$(bl,{title:`Event ${f.id}`,data:f,onOpen:l}))))))))}function Hp({logs:u,onRaw:l}){return $(du,{title:"服务日志",eyebrow:"Core Recent"},u.length===0?$(jl,{title:"暂无日志",text:"backend-core 内存日志会在请求和 provider 事件后出现"}):$("div",{className:"log-list"},u.slice(-80).reverse().map((f,r)=>$("article",{key:r,className:`log-row ${f.level||"info"}`},$("span",null,qu(f.ts)),$("b",null,f.level||"info"),$("strong",null,f.message||"log"),$(sn,{data:f.data,empty:"无附加字段"}),$(bl,{title:`Log ${f.message||r}`,data:f,onOpen:l})))))}function Bp({nodes:u,onRaw:l}){return $(du,{title:"节点清单",eyebrow:`${u.length} Providers`},u.length===0?$(jl,{title:"暂无 Provider 节点",text:"确认 provider-gateway 已连接 provider ingress"}):$("div",{className:"table-wrap"},$("table",{className:"node-list-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"Provider"),$("th",null,"网关版本"),$("th",null,"运维可用性"),$("th",null,"资源标签"),$("th",null,"连接时间"),$("th",null,"最后心跳"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.providerId},$("td",null,$(wl,{status:f.status})),$("td",null,$("strong",null,f.name),$("code",null,f.providerId)),$("td",null,$("div",{className:"gateway-cell"},$(wy,{node:f}),$("span",null,h_(f)))),$("td",null,$(_7,{node:f})),$("td",null,$(OL,{labels:f.labels,limit:5})),$("td",null,qu(f.connectedAt)),$("td",null,qu(f.lastHeartbeat)),$("td",null,$(bl,{title:`Provider ${f.providerId}`,data:f,onOpen:l,testId:`raw-node-table-${ef(f.providerId)}`}))))))))}function Vp({nodes:u}){let l=b_(()=>{let f=[];for(let r of u)for(let[n,i]of WL(r.labels))f.push({providerId:r.providerId,name:r.name,key:n,value:i});return f},[u]);return $(du,{title:"资源标签",eyebrow:"Structured Labels"},l.length===0?$(jl,{title:"暂无标签",text:"provider-gateway 注册消息会同步资源标签"}):$("div",{className:"label-matrix"},l.map((f)=>$("article",{key:`${f.providerId}-${f.key}`,className:"label-card"},$("span",null,f.key),$("strong",null,hi(f.value)),$("code",null,f.providerId)))))}function Dp({nodes:u}){return $(du,{title:"心跳状态",eyebrow:"Provider Liveness"},u.length===0?$(jl,{title:"无心跳",text:"等待 provider 注册和 heartbeat"}):$("div",{className:"heartbeat-list"},u.map((l)=>$("article",{key:l.providerId,className:"heartbeat-row"},$("span",{className:`pulse ${l.status}`}),$("div",null,$("strong",null,l.name),$("code",null,l.providerId)),$("div",null,$("span",null,"connected"),$("b",null,qu(l.connectedAt))),$("div",null,$("span",null,"last heartbeat"),$("b",null,qu(l.lastHeartbeat)))))))}function Xp({nodes:u,systemStatuses:l,tasks:f,onRaw:r,refresh:n}){let[i,y]=bu(""),t=b_(()=>u.map((w)=>{let L=l.find((U)=>U.providerId===w.providerId);return{...w,systemCurrent:L?.current||null,systemHistory:L?.history||[],systemUpdatedAt:L?.updatedAt||null}}),[u,l]),_=t.find((w)=>w.providerId===i)||t[0]||null;if(J0(()=>{if(!i&&t[0])y(t[0].providerId)},[t.length,i]),!_)return $(jl,{title:"暂无资源监控",text:"等待 provider 上报 CPU、内存和硬盘指标"});let c=_.systemCurrent,A=_.systemHistory||[],j=c?.cpu||{},F=c?.memory||{},J=c?.disk||{},Q=A.length>0?A:c?[{at:c.collectedAt,cpuPercent:Mu(j.percent),memoryPercent:Mu(F.percent),diskPercent:Mu(J.percent)}]:[];return $("div",{className:"monitor-page","data-testid":"node-monitor-page"},$("div",{className:"docker-node-strip"},t.map((w)=>$("button",{key:w.providerId,type:"button",className:`docker-node-tile ${_.providerId===w.providerId?"active":""}`,onClick:()=>y(w.providerId)},$("span",{className:`pulse ${w.status}`}),$("strong",null,w.name),$("code",null,w.providerId),$("span",null,w.systemCurrent?`CPU ${In(w.systemCurrent.cpu?.percent)} / MEM ${In(w.systemCurrent.memory?.percent)}`:"等待指标")))),$("div",{className:"monitor-layout"},$(du,{title:"任务管理器视图",eyebrow:_.name,className:"monitor-main-panel",actions:c?$(bl,{title:`System ${_.providerId}`,data:{current:c,history:A},onOpen:r}):null},!c?$(jl,{title:"系统指标未上报",text:"provider-gateway 会周期性采集 /proc 与 df,并保存历史曲线"}):$("div",null,$("div",{className:"monitor-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Node Performance"),$("h3",null,_.name),$("div",{className:"docker-meta"},$("span",null,`${j.cores||0} CPU cores`),$("span",null,`load ${Mu(j.load1).toFixed(2)} / ${Mu(j.load5).toFixed(2)} / ${Mu(j.load15).toFixed(2)}`),$("span",null,`memory actual ${Tf(F.usedBytes)} / ${Tf(F.totalBytes)}`),$("span",null,`disk ${Tf(J.usedBytes)} / ${Tf(J.totalBytes)}`))),$(wl,{status:c.ok?"online":"warn"},c.ok?"METRICS READY":"METRICS DEGRADED")),$("div",{className:"monitor-chart-grid"},$(f7,{title:"CPU",metricKey:"cpuPercent",current:j.percent,points:Q,detail:`${j.cores||0} cores / load ${Mu(j.load1).toFixed(2)}`,tone:"cpu",testId:"metric-chart-cpu"}),$(f7,{title:"Memory",metricKey:"memoryPercent",current:F.percent,points:Q,detail:`${Tf(F.usedBytes)} actual / ${Tf(F.cacheBytes)} cache excluded`,tone:"memory",testId:"metric-chart-memory"}),$(f7,{title:"Disk",metricKey:"diskPercent",current:J.percent,points:Q,detail:`${J.path||"/"} mounted ${J.mount||"--"}`,tone:"disk",testId:"metric-chart-disk"})),$("div",{className:"monitor-summary-grid"},$(cl,{label:"CPU 当前",value:In(j.percent),hint:`history ${Q.length} samples`,tone:"ok"}),$(cl,{label:"实际内存",value:Tf(F.usedBytes),hint:`${In(F.percent)} 不含缓存`}),$(cl,{label:"硬盘已用",value:Tf(J.usedBytes),hint:In(J.percent)}),$(cl,{label:"更新时间",value:qu(_.systemUpdatedAt||c.collectedAt),hint:_.providerId})),$(Sp,{current:c,onRaw:r}))),$("div",{className:"monitor-side-stack"},$(xp,{provider:_,refresh:n,onRaw:r}),$(hp,{provider:_,tasks:f,onRaw:r,limit:5}),$(du,{title:"采样说明",eyebrow:"Retention"},$("div",{className:"monitor-note-list"},$("article",null,$("b",null,"CPU"),$("span",null,"从 /proc/stat 计算相邻采样差值,首个采样用 load/cores 近似")),$("article",null,$("b",null,"Memory"),$("span",null,"实际内存 = MemTotal - MemFree - Buffers - Cached - SReclaimable + Shmem,不把 page cache / buffer 计入占用")),$("article",null,$("b",null,"Disk"),$("span",null,"使用 df -PB1 对配置路径采样,默认监控根文件系统")),$("article",null,$("b",null,"Process"),$("span",null,"从 /proc/[pid] 采集进程 CPU、实际内存 RSS、线程数和磁盘 I/O 速率;表格默认按内存占用降序")))))))}function cL(u,l){if(l==="memory")return Mu(u.rssBytes);if(l==="cpu")return Mu(u.cpuPercent);if(l==="disk")return Mu(u.readBytesPerSecond)+Mu(u.writeBytesPerSecond);if(l==="pid")return Mu(u.pid);if(l==="threads")return Mu(u.threads);if(l==="runtime")return Mu(u.elapsedSeconds);if(l==="user")return String(u.user||"");return String(u.name||u.command||"")}function AL({value:u,label:l,tone:f}){let r=Math.max(1,Math.min(100,Mu(u)));return $("div",{className:`process-meter ${f||""}`},$("span",{style:{width:`${r}%`}}),$("b",null,l))}function Sp({current:u,onRaw:l}){let[f,r]=bu({key:"memory",direction:"desc"}),n=gn.default.useContext(n7),i=u?.processSummary&&typeof u.processSummary==="object"?u.processSummary:{},y=Array.isArray(u?.processes)?u.processes:[],t=b_(()=>{let c=f.direction==="asc"?1:-1;return[...y].sort((A,j)=>{let F=cL(A,f.key),J=cL(j,f.key);if(typeof F==="string"||typeof J==="string")return String(F).localeCompare(String(J),"zh-CN")*c;return(F-J)*c||Mu(A.pid)-Mu(j.pid)})},[y,f.key,f.direction]),_=(c,A)=>{let j=f.key===A,F=j?f.direction==="asc"?"ascending":"descending":"none";return $("th",{"aria-sort":F},$("button",{type:"button",className:`process-sort-button ${j?"active":""}`,"data-testid":`process-sort-${A}`,onClick:()=>r((J)=>({key:A,direction:J.key===A&&J.direction==="desc"?"asc":"desc"}))},c,$("span",null,j?f.direction==="desc"?"↓":"↑":"↕")))};return $("section",{className:"process-resource-panel","data-testid":"process-resource-panel"},$("div",{className:"process-resource-head"},$("div",null,$("p",{className:"panel-eyebrow"},"Windows Resource Monitor Style"),$(nl,{title:"进程资源占用",level:3,loading:n})),$("div",{className:"process-resource-actions"},$("span",{className:"data-chip"},"默认按内存排序"),$("span",{className:"data-chip"},`${Mu(i.visible,t.length)} / ${Mu(i.total,t.length)} 进程`),$(bl,{title:"Process Resource Snapshot",data:{processSummary:i,processes:y},onOpen:l,testId:"raw-process-resources"}))),t.length===0?$(jl,{title:"暂无进程资源数据",text:"等待 provider-gateway 上报 /proc/[pid] 采样;旧版 provider 需要先升级到支持进程资源表的版本"}):$("div",{className:"process-table-wrap"},$("table",{className:"process-resource-table","data-testid":"process-resource-table"},$("thead",null,$("tr",null,_("进程","name"),_("PID","pid"),_("用户","user"),$("th",null,"状态"),_("CPU","cpu"),_("内存","memory"),$("th",null,"RSS"),_("磁盘 I/O","disk"),_("线程","threads"),_("运行时长","runtime"))),$("tbody",null,t.map((c)=>{let A=Mu(c.readBytesPerSecond)+Mu(c.writeBytesPerSecond);return $("tr",{key:`${c.pid}-${c.startedAt}`,"data-testid":`process-row-${ef(c.pid)}`,"data-memory-bytes":String(Mu(c.rssBytes)),"data-cpu-percent":String(Mu(c.cpuPercent)),"data-disk-bps":String(A),"data-pid":String(Mu(c.pid))},$("td",null,$("div",{className:"process-name-cell"},$("strong",null,c.name||"--"),$("span",{className:"process-command"},c.command||"--"))),$("td",null,$("code",null,c.pid||"--")),$("td",null,c.user||`uid:${c.uid??"--"}`),$("td",null,$("span",{className:`process-state state-${ef(c.state||"unknown")}`},c.state||"?")),$("td",null,$(AL,{value:c.cpuPercent,label:jp(c.cpuPercent),tone:"cpu"})),$("td",null,$(AL,{value:c.memoryPercent,label:In(c.memoryPercent),tone:"memory"})),$("td",null,Tf(c.rssBytes)),$("td",null,$("div",{className:"process-io-cell"},$("strong",null,l7(A)),$("span",null,`R ${l7(c.readBytesPerSecond)} / W ${l7(c.writeBytesPerSecond)}`))),$("td",null,c.threads||0),$("td",null,b0(Mu(c.elapsedSeconds))))})))))}function f7({title:u,metricKey:l,current:f,points:r,detail:n,tone:i,testId:y}){let t=r.map((F)=>Math.max(0,Math.min(100,Mu(F[l])))),_=t.length>1?t:[t[0]||0,t[0]||0],c=_.length<=1?100:100/(_.length-1),A=_.map((F,J)=>`${(J*c).toFixed(2)},${(46-F*0.42).toFixed(2)}`).join(" "),j=`0,48 ${A} 100,48`;return $("article",{className:`metric-chart ${i}`,"data-testid":y},$("div",{className:"metric-chart-head"},$("div",null,$("span",null,u),$("strong",null,In(f))),$("code",null,`${r.length} pts`)),$("svg",{viewBox:"0 0 100 48",preserveAspectRatio:"none",role:"img","aria-label":`${u} usage curve`},$("polygon",{points:j}),$("polyline",{points:A}),$("line",{x1:"0",x2:"100",y1:"24",y2:"24"})),$("div",{className:"metric-chart-foot"},$("span",null,"0%"),$("span",null,n),$("span",null,"100%")))}function v0(u){return Array.isArray(u)?u:[]}function Yp(u){let l=v0(u?.core?.requests?.componentSummary);return[...v0(u?.frontend?.requests?.componentSummary),...l].sort((r,n)=>Mu(n.requestCount)-Mu(r.requestCount))}function pp(u){let l=v0(u?.core?.operations?.summary);return[...v0(u?.frontend?.operations?.summary),...l].sort((r,n)=>Mu(n.count)-Mu(r.count))}function mp(u){let l=v0(u?.core?.requests?.recentFailures).map((r)=>({source:"backend",...r}));return[...v0(u?.frontend?.requests?.recentFailures).map((r)=>({source:"frontend",...r})),...l].sort((r,n)=>(Q0(n.at)??0)-(Q0(r.at)??0)).slice(0,20)}function Pp(u){let l=v0(u?.core?.operations?.recentSlowOperations);return[...v0(u?.frontend?.operations?.recentSlowOperations),...l].sort((r,n)=>Mu(n.durationMs)-Mu(r.durationMs)).slice(0,20)}function Cp(u){let l=performance.memory,f=Number(l?.usedJSHeapSize);if(Number.isFinite(f)&&f>0)return f;let r=Number(u?.appBundleBytes);if(Number.isFinite(r)&&r>0)return r;return Mu(u?.process?.heapUsedBytes)}function Mp({points:u}){let l=v0(u),f=l.map((F)=>Mu(F.mb)),r=Math.max(1,...f),n=Math.max(0,Math.min(...f,0)),i=Math.max(1,r-n),y=l.length>1?l:[...l,...l],t=y.length<=1?100:100/(y.length-1),_=y.map((F,J)=>{let Q=Mu(F.mb);return`${(J*t).toFixed(2)},${(48-(Q-n)/i*42).toFixed(2)}`}).join(" "),c=`0,50 ${_} 100,50`,A=l.at(-1),j=l[0];return $("article",{className:"performance-memory-card","data-testid":"performance-memory-chart"},$("div",{className:"performance-memory-head"},$("strong",null,`Bwebui: ${A?`${Mu(A.mb).toFixed(1)}MB`:"--"}`),$("span",null,l.length>0?`${l.length} samples`:"等待采样")),$("svg",{viewBox:"0 0 100 50",preserveAspectRatio:"none",role:"img","aria-label":"Bwebui memory trend"},$("polygon",{points:c}),$("polyline",{points:_}),$("line",{x1:"0",x2:"100",y1:"25",y2:"25"})),$("div",{className:"performance-axis-row"},$("span",null,j?tl(new Date(j.at)):"--"),$("span",null,"时间"),$("span",null,A?tl(new Date(A.at)):"--")),$("div",{className:"performance-axis-row"},$("span",null,`${n.toFixed(1)}`),$("span",null,"(MB)"),$("span",null,`${r.toFixed(1)}`)))}function Rp({onRaw:u}){let[l,f]=bu({core:null,frontend:null}),[r,n]=bu([]),[i,y]=bu(""),[t,_]=bu(!1),[c,A]=bu(null),[j,F]=bu(!1);async function J(){_(!0),y("");try{let[S,p]=await Promise.all([Tu(`${gu.apiBaseUrl}/performance`,{cache:"no-store"}),Tu(`${gu.apiBaseUrl}/frontend-performance`,{cache:"no-store"})]);f({core:S,frontend:p});let O=Cp(p);n((m)=>[...m,{at:new Date().toISOString(),mb:O/1048576}].slice(-80))}catch(S){y(Ou(S,"性能指标加载失败"))}finally{_(!1)}}J0(()=>{J();let S=setInterval(()=>void J(),5000);return()=>clearInterval(S)},[]);async function Q(){F(!0),y(""),A(null);try{let S=await Tu(`${gu.apiBaseUrl}/code-queue-load-test`,{method:"POST",body:JSON.stringify({targetMs:1000,timeoutMs:90000,url:gu.frontendPublicUrl||window.location.origin})});A(S),J()}catch(S){y(Ou(S,"Code Queue Playwright 测量失败"))}finally{F(!1)}}let w=Yp(l),L=mp(l),U=pp(l),N=Pp(l),q=l.core?.process||{},W=l.frontend?.process||{},z=l.core?.database?.codeQueueStorage||{},Z=Mu(z.total),H=c?.result||{},E=Mu(H.wallMs,NaN),D=Mu(H.networkIdleMs,NaN),h=H.withinTarget===!0,V=j?"running":c===null?"idle":c.measurementOk===!0?h?"passed":"slow":"failed";return $("div",{className:"performance-page","data-testid":"performance-page"},$("div",{className:"performance-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Unified Performance"),$(nl,{title:"性能面板",loading:t||j}),$("p",null,"按组件统计 HTTP 请求、失败率、P95 延迟,并汇总 backend/frontend 内部操作耗时。")),$("div",{className:"inline-actions"},$("button",{type:"button",className:"ghost-btn",onClick:()=>void Q(),disabled:j,"data-testid":"code-queue-load-test-button"},j?"测试中...":"测试 Code Queue 加载"),$("button",{type:"button",className:"ghost-btn",onClick:()=>void J(),disabled:t,"data-testid":"performance-refresh-button"},t?"刷新中":"刷新"),$(bl,{title:"Performance Snapshot",data:l,onOpen:u,testId:"raw-performance"}))),$(il,{error:i}),$("div",{className:"performance-top-grid"},$(Mp,{points:r}),$("div",{className:"performance-metric-stack"},$(cl,{label:"backend RSS",value:Tf(q.rssBytes),hint:`heap ${Tf(q.heapUsedBytes)}`}),$(cl,{label:"frontend RSS",value:Tf(W.rssBytes),hint:`bundle ${Tf(l.frontend?.appBundleBytes)}`}),$(cl,{label:"Codex PG 任务",value:Z||"--",hint:z.ok?"unidesk_code_queue_tasks":"等待表初始化",tone:z.ok?"ok":"warn"}),$(cl,{label:"请求样本",value:Mu(l.core?.requests?.sampleCount)+Mu(l.frontend?.requests?.sampleCount),hint:"rolling window 3000"}))),$(du,{title:"Code Queue 加载基准",eyebrow:"Playwright / target <1s",className:"codex-load-test-panel",loading:j,actions:$("div",{className:"panel-actions"},$("button",{type:"button",className:"primary-btn",onClick:()=>void Q(),disabled:j,"data-testid":"code-queue-load-test-panel-button"},j?"正在运行 Playwright...":"手动触发测试"),c?$(bl,{title:"Code Queue Load Test",data:c,onOpen:u,testId:"raw-code-queue-load-test"}):null)},$("div",{className:"codex-load-test-grid","data-testid":"code-queue-load-test-result"},$(cl,{label:"总耗时",value:j?"运行中":Number.isFinite(E)?Nr(E):"--",hint:c===null?"点击按钮启动远端 Playwright":`目标 ${Nr(H.targetMs||1000)} / ${H.url||"Code Queue"}`,tone:V==="passed"?"ok":V==="failed"||V==="slow"?"warn":""}),$(cl,{label:"判定",value:j?"RUNNING":V==="passed"?"PASS <1s":V==="slow"?"SLOW":V==="failed"?"FAILED":"--",hint:c?.measurementOk===!1?String(c.error||H.error||"measurement failed").slice(0,120):"导航开始 -> DOMContentLoaded -> data-load-state=complete",tone:V==="passed"?"ok":V==="idle"||V==="running"?"":"fail"}),$(cl,{label:"Network idle",value:Number.isFinite(D)?Nr(D):"--",hint:`DOMContentLoaded ${Nr(H.domContentLoadedMs)} / ${H.networkIdleReached===!1?"未在 5s 内空闲":"已空闲"}`,tone:Number.isFinite(D)&&D<=1000?"ok":"warn"}),$(cl,{label:"组件耗时",value:Number.isFinite(Mu(H.componentLoadMs,NaN))?Nr(H.componentLoadMs):"--",hint:`queue ${Nr(H.queueMs)} / detail ${Nr(H.detailMs)}`,tone:Mu(H.componentLoadMs)>1000?"warn":"ok"}),$(cl,{label:"Trace 规模",value:Number.isFinite(Mu(H.transcriptRows,NaN))?String(H.transcriptRows):"--",hint:`${H.visibleTaskCount??0} visible tasks / ${H.partial?"preview":"complete"}`})),j?$("div",{className:"performance-empty-line"},"正在通过 main-server Host SSH 启动 Playwright,完成后会显示 wall time、组件耗时和最慢 API。"):null,c&&Array.isArray(H.slowestApi)&&H.slowestApi.length>0?$("div",{className:"table-wrap performance-table-wrap compact codex-load-api-table"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["API","状态","耗时"].map((S)=>$("th",{key:S},S)))),$("tbody",null,H.slowestApi.slice(0,5).map((S,p)=>$("tr",{key:`${S.url}-${p}`},$("td",null,$("code",null,S.url)),$("td",null,S.status),$("td",null,Nr(S.durationMs))))))):null),$("div",{className:"performance-grid"},$(du,{title:"组件汇总",eyebrow:"Requests",loading:t},w.length===0?$(jl,{title:"暂无请求样本",text:"刷新几次或打开页面后会自动形成组件统计"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["组件","请求数","失败数","失败率","平均延迟","P95"].map((S)=>$("th",{key:S},S)))),$("tbody",null,w.map((S)=>$("tr",{key:S.component},$("td",null,$("code",null,S.component)),$("td",null,S.requestCount),$("td",null,S.failureCount),$("td",null,In(Mu(S.failureRate)*100)),$("td",null,Nr(S.averageLatencyMs)),$("td",null,Nr(S.p95LatencyMs)))))))),$(du,{title:"最近失败请求",eyebrow:"Failures",loading:t},L.length===0?$("div",{className:"performance-empty-line"},"最近没有失败请求"):$("div",{className:"table-wrap performance-table-wrap compact"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["时间","来源","组件","状态","路径"].map((S)=>$("th",{key:S},S)))),$("tbody",null,L.map((S,p)=>$("tr",{key:`${S.at}-${p}`},$("td",null,qu(S.at)),$("td",null,S.source),$("td",null,$("code",null,S.component)),$("td",null,$(wl,{status:"failed"},S.status)),$("td",null,$("code",null,S.path)))))))),$(du,{title:"内部操作汇总",eyebrow:"Operations",loading:t},U.length===0?$(jl,{title:"暂无内部操作样本",text:"API 查询和代理请求会自动记录内部操作耗时"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["服务","操作","次数","平均延迟","P95"].map((S)=>$("th",{key:S},S)))),$("tbody",null,U.map((S)=>$("tr",{key:`${S.service}-${S.operation}`},$("td",null,S.service),$("td",null,$("code",null,S.operation)),$("td",null,S.count),$("td",null,Nr(S.averageLatencyMs)),$("td",null,Nr(S.p95LatencyMs)))))))),$(du,{title:"最近慢操作",eyebrow:"Slowest",loading:t},N.length===0?$(jl,{title:"暂无慢操作",text:"后端会记录最近窗口内耗时最高的内部操作"}):$("div",{className:"table-wrap performance-table-wrap"},$("table",{className:"performance-table"},$("thead",null,$("tr",null,["时间","操作","耗时","结果","细节"].map((S)=>$("th",{key:S},S)))),$("tbody",null,N.map((S,p)=>$("tr",{key:`${S.at}-${S.operation}-${p}`},$("td",null,qu(S.at)),$("td",null,$("code",null,S.operation)),$("td",null,Nr(S.durationMs)),$("td",null,S.ok?"成功":"失败"),$("td",null,S.detail||"-")))))))))}function xp({provider:u,refresh:l,onRaw:f}){let[r,n]=bu(""),[i,y]=bu(null),[t,_]=bu("");async function c(A){n(A),_("");try{let j=await Tu(`${gu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:u.providerId,command:"provider.upgrade",payload:{mode:A,source:"frontend-resource-monitor",requestedAt:new Date().toISOString()}})});y({mode:A,...j}),await l()}catch(j){_(Ou(j,"升级命令下发失败"))}finally{n("")}}return $(du,{title:"Provider Gateway 升级",eyebrow:"Remote Control",loading:Boolean(r)},$("div",{className:"upgrade-control","data-testid":"provider-upgrade-control"},$("p",null,"通过 UniDesk WebSocket 向当前计算节点下发 provider.upgrade;预检只生成升级计划,执行升级会调度节点本地 updater 容器。"),$("div",{className:"upgrade-target-line"},$("span",null,"指定 Provider"),$("code",null,u.providerId),$(wy,{node:u})),$("div",{className:"upgrade-actions"},$("button",{type:"button",className:"ghost-btn",disabled:Boolean(r),onClick:()=>c("plan"),"data-testid":"upgrade-plan-button"},r==="plan"?"预检中":"预检升级"),$("button",{type:"button",className:"ghost-btn danger",disabled:Boolean(r),onClick:()=>c("schedule"),"data-testid":"upgrade-schedule-button"},r==="schedule"?"调度中":"执行升级")),$(il,{error:t}),i?$("div",{className:"upgrade-result"},$(wl,{status:i.status||"queued"},i.status||"queued"),$("span",null,`${i.mode==="schedule"?"执行升级":"预检升级"} 已下发`),$("span",null,`指定版本 ${t7(wL(u))}`),$("code",null,i.taskId||"--"),$(bl,{title:"Provider Upgrade Dispatch",data:i,onOpen:f})):$("span",{className:"muted"},"升级任务结果会进入任务历史;执行升级可能导致 provider 短暂重连。")))}function BL({records:u,onRaw:l,compact:f=!1}){if(u.length===0)return $(jl,{title:"暂无远程更新记录",text:"该节点还没有 provider.upgrade 任务;执行预检或升级后会在这里形成结构化记录"});return $("div",{className:`upgrade-record-table-wrap table-wrap ${f?"compact":""}`},$("table",{className:"upgrade-record-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"模式"),$("th",null,"任务"),$("th",null,"来源"),$("th",null,"耗时"),$("th",null,"策略"),$("th",null,"Gateway 版本"),$("th",null,"结果记录"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,u.map((r)=>$("tr",{key:r.id,"data-testid":`gateway-upgrade-record-${ef(r.id)}`},$("td",null,$(wl,{status:r.status})),$("td",null,$("span",{className:`mode-chip ${S6(r)}`},S6(r)==="schedule"?"执行升级":"预检")),$("td",null,$("strong",null,"provider.upgrade"),$("code",null,r.id)),$("td",null,Qp(r)),$("td",null,$(DL,{task:r})),$("td",null,Np(r)),$("td",null,$("span",{className:"version-chip"},zL(r))),$("td",null,$("span",{className:`upgrade-outcome ${String(r.status||"").toLowerCase()}`},TL(r))),$("td",null,qu(r.updatedAt)),$("td",null,$(bl,{title:`Provider Upgrade Task ${r.id}`,data:v_(r),onOpen:l})))))))}function hp({provider:u,tasks:l,onRaw:f,limit:r=5}){let n=EL(l,u.providerId).slice(0,r);return $(du,{title:"远程更新记录",eyebrow:u.providerId,actions:$(wy,{node:u}),className:"provider-upgrade-records-panel"},$("div",{"data-testid":`provider-upgrade-records-${ef(u.providerId)}`},$(BL,{records:n,onRaw:f,compact:!0})))}function bp({nodes:u,tasks:l,onRaw:f}){let r=b_(()=>u.map((i)=>{let y=EL(l,i.providerId);return{node:i,records:y,latest:qp(y),capabilities:LL(i)}}),[u,l]),n=r.reduce((i,y)=>i+y.records.length,0);return $("div",{className:"gateway-page","data-testid":"gateway-version-page"},$(du,{title:"Provider Gateway 版本",eyebrow:`${u.length} Providers / ${n} 更新记录`},u.length===0?$(jl,{title:"暂无 Provider 节点",text:"等待 provider-gateway 注册后显示版本号和升级记录"}):$("div",{className:"table-wrap gateway-version-table-wrap"},$("table",{className:"gateway-version-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"Provider"),$("th",null,"Gateway 版本"),$("th",null,"升级策略"),$("th",null,"运维可用性"),$("th",null,"运行时间"),$("th",null,"能力"),$("th",null,"最近远程更新"),$("th",null,"操作"))),$("tbody",null,r.map((i)=>$("tr",{key:i.node.providerId},$("td",null,$(wl,{status:i.node.status})),$("td",null,$("strong",null,i.node.name),$("code",null,i.node.providerId)),$("td",null,$(wy,{node:i.node})),$("td",null,h_(i.node)),$("td",null,$(_7,{node:i.node})),$("td",null,yL(i.node)?qu(yL(i.node)):"待新版上报"),$("td",null,$("div",{className:"capability-row"},i.capabilities.length===0?$("span",{className:"muted"},"未声明"):i.capabilities.slice(0,5).map((y)=>$("span",{key:y,className:"data-chip"},y)))),$("td",null,i.latest?$("div",{className:"latest-upgrade-cell"},$(wl,{status:i.latest.status}),$("span",null,`${S6(i.latest)==="schedule"?"执行升级":"预检"} / ${qu(i.latest.updatedAt)}`),$("small",null,`Gateway ${zL(i.latest)}`),$("small",null,TL(i.latest))):$("span",{className:"muted"},"暂无记录")),$("td",null,$(bl,{title:`Provider ${i.node.providerId}`,data:i.node,onOpen:f})))))))),$(du,{title:"远程更新记录",eyebrow:"Structured provider.upgrade records"},u.length===0?$(jl,{title:"暂无记录",text:"没有 provider 节点时不会生成远程更新记录"}):$("div",{className:"gateway-record-grid"},r.map((i)=>$("article",{key:i.node.providerId,className:"gateway-record-card","data-testid":`gateway-records-${ef(i.node.providerId)}`},$("div",{className:"gateway-record-head"},$("div",null,$("strong",null,i.node.name),$("code",null,i.node.providerId)),$(wy,{node:i.node})),$("div",{className:"gateway-record-meta"},$("span",null,`心跳 ${qu(i.node.lastHeartbeat)}`),$("span",null,`策略 ${h_(i.node)}`),$("span",null,`${i.records.length} 条记录`)),$(BL,{records:i.records.slice(0,8),onRaw:f,compact:!0}))))))}function vp(u){if(u==="running")return"online";if(u==="paused"||u==="restarting")return"warn";if(u==="exited"||u==="dead")return"offline";return"internal"}function VL(u){return/^[a-f0-9]{48,64}$/i.test(u)}function x_(u){let l=String(u?.name||""),f=String(u?.labels||"");return l==="unidesk_pgdata_10gb"||f.includes("com.docker.compose.volume=unidesk_pgdata_10gb")||l.toLowerCase().includes("pgdata")}function jL(u){let l=String(u?.name||""),f=String(u?.labels||"");if(x_(u))return 0;if(f.includes("com.docker.compose.project=unidesk"))return 1;if(!VL(l))return 2;return 3}function kp(u){return[...u].sort((l,f)=>{let r=jL(l)-jL(f);if(r!==0)return r;return String(l.name||"").localeCompare(String(f.name||""))})}function Ip({nodes:u,dockerStatuses:l,onRaw:f}){let[r,n]=bu(""),i=b_(()=>u.map((N)=>{let q=l.find((W)=>W.providerId===N.providerId);return{...N,dockerStatus:q?.dockerStatus||null,dockerUpdatedAt:q?.updatedAt||null}}),[u,l]),y=i.find((N)=>N.providerId===r)||i[0]||null;if(J0(()=>{if(!r&&i[0])n(i[0].providerId)},[i.length,r]),!y)return $(jl,{title:"暂无 Docker 节点",text:"等待 provider 上报 Docker daemon 状态"});let t=y.dockerStatus,_=y.providerId==="main-server",c=t?.counts||{},A=t?.daemon||{},j=t?.containers||[],F=t?.images||[],J=kp(t?.volumes||[]),Q=_?J.find(x_):null,w=t?.networks||[],L=j.filter((N)=>N.state==="running"),U=j.filter((N)=>N.state!=="running");return $("div",{className:"docker-page","data-testid":"docker-status-page"},$("div",{className:"docker-node-strip"},i.map((N)=>$("button",{key:N.providerId,type:"button",className:`docker-node-tile ${y.providerId===N.providerId?"active":""}`,onClick:()=>n(N.providerId)},$("span",{className:`pulse ${N.status}`}),$("strong",null,N.name),$("code",null,N.providerId),$("span",null,N.dockerStatus?`Docker ${N.dockerStatus.ok?"ready":"degraded"}`:"等待上报")))),$("div",{className:"docker-layout"},$(du,{title:"Docker Desktop 视图",eyebrow:y.name,className:"docker-main-panel",actions:t?$(bl,{title:`Docker ${y.providerId}`,data:t,onOpen:f}):null},!t?$(jl,{title:"Docker 状态未上报",text:"provider-gateway 会在连接后周期性采集 docker info / ps / images / volume / network"}):$("div",null,$("div",{className:"docker-hero"},$("div",null,$("p",{className:"panel-eyebrow"},"Daemon"),$("h3",null,A.name||y.providerId),$("div",{className:"docker-meta"},$("span",null,A.serverVersion?`Engine ${A.serverVersion}`:"Engine --"),$("span",null,A.operatingSystem||"OS --"),$("span",null,A.architecture||"arch --"),$("span",null,`${A.cpus||0} CPU / ${Tf(A.memoryBytes)}`))),$(wl,{status:t.ok?"online":"warn"},t.ok?"Docker Ready":"Docker Degraded")),$("div",{className:"docker-metrics"},$(cl,{label:"Containers",value:c.containers??j.length,hint:`${c.running??L.length} running / ${c.stopped??U.length} stopped`,tone:"ok"}),$(cl,{label:"Images",value:c.images??F.length,hint:`${c.daemonImages??c.images??F.length} daemon images`}),$(cl,{label:"Volumes",value:c.volumes??J.length,hint:_?Q?"database volume visible":"database volume missing":"node local volumes",tone:Q?"ok":""}),$(cl,{label:"Networks",value:c.networks??w.length,hint:A.driver?`driver ${A.driver}`:"docker networks"})),_?$(gp,{volume:Q,volumeCount:J.length}):null,$("div",{className:"docker-section-head"},$("h3",null,"Containers"),$("span",null,`updated ${qu(y.dockerUpdatedAt||t.collectedAt)}`)),$("div",{className:"docker-container-table table-wrap","data-testid":"docker-container-table"},$("table",null,$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"容器"),$("th",null,"镜像"),$("th",null,"端口"),$("th",null,"运行时间"),$("th",null,"重启策略"),$("th",null,"PID"),$("th",null,"大小"))),$("tbody",null,j.length===0?$("tr",null,$("td",{colSpan:8},"暂无容器")):j.map((N)=>$("tr",{key:`${N.id}-${N.name}`},$("td",null,$(wl,{status:vp(N.state)},N.state||"unknown")),$("td",null,$("strong",null,N.name||"--"),$("code",null,N.id||"--")),$("td",null,N.image||"--"),$("td",null,N.ports||$("span",{className:"muted"},"未发布")),$("td",null,N.runningFor||N.status||"--"),$("td",null,N.restartPolicy?$(wl,{status:N.restartPolicy==="always"?"online":"warn"},N.restartPolicy):"--"),$("td",null,N.pidMode?$("code",null,N.pidMode):"--"),$("td",null,N.size||"--")))))))),$("div",{className:"docker-side-stack"},$(r7,{title:"Images",items:F,render:(N)=>$("article",{key:`${N.id}-${N.repository}`,className:"docker-side-row"},$("strong",null,`${N.repository}:${N.tag}`),$("span",null,N.size||"--"),$("code",null,N.id||"--"))}),$(r7,{title:"Volumes",items:J,limit:J.length,render:(N)=>$("article",{key:N.name,className:`docker-side-row volume-row ${_&&x_(N)?"database-volume":""}`,"data-testid":_&&x_(N)?"database-volume-row":void 0},$("strong",null,N.name),$("span",null,_&&x_(N)?"PostgreSQL":VL(String(N.name||""))?"anonymous":"named"),$("code",null,N.mountpoint||N.driver||N.scope||"--"))}),$(r7,{title:"Networks",items:w,render:(N)=>$("article",{key:N.id||N.name,className:"docker-side-row"},$("strong",null,N.name),$("span",null,N.driver||"--"),$("code",null,N.id||"--"))}))))}function gp({volume:u,volumeCount:l}){return $("section",{className:`docker-volume-focus ${u?"ready":"missing"}`,"data-testid":"database-volume-card"},$("div",{className:"volume-focus-head"},$("span",{className:"panel-eyebrow"},"Database Named Volume"),$(wl,{status:u?"online":"warn"},u?"FOUND":"MISSING")),u?$("div",{className:"volume-focus-body"},$("strong",null,u.name),$("span",null,"PostgreSQL data volume for unidesk-database"),$("div",{className:"volume-route"},$("code",null,u.mountpoint||"/var/lib/docker/volumes/unidesk_pgdata_10gb/_data"),$("span",null,"->"),$("code",null,"unidesk-database:/var/lib/postgresql/data")),$("div",{className:"docker-meta compact"},$("span",null,`driver ${u.driver||"--"}`),$("span",null,`scope ${u.scope||"--"}`),$("span",null,`${l} volumes reported`))):$("div",{className:"volume-focus-body"},$("strong",null,"unidesk_pgdata_10gb"),$("span",null,"当前 Docker 快照没有发现数据库命名卷;请检查 provider-gateway 的 Docker volume 上报。")))}function r7({title:u,items:l,render:f,limit:r}){let n=l.slice(0,r??12),i=Math.max(0,l.length-n.length);return $(du,{title:u,eyebrow:`${l.length} items`,className:"docker-side-panel"},l.length===0?$(jl,{title:`暂无 ${u}`,text:"等待 Docker 状态采集"}):$("div",{className:"docker-side-list"},n.map(f),i>0?$("div",{className:"docker-side-more"},`+ ${i} more`):null))}function sp({microservices:u,onRaw:l,onNavigate:f}){let r=u.filter((n)=>_L(n).public===!1);return $("div",{className:"microservice-page","data-testid":"microservice-catalog-page"},$(du,{title:"用户服务目录",eyebrow:"Provider Mounted User Services"},$("div",{className:"metric-grid"},$(cl,{label:"服务总数",value:u.length,hint:"config.json 用户服务登记"}),$(cl,{label:"私有后端",value:r.length,hint:"不直接暴露公网",tone:"ok"}),$(cl,{label:"D601 服务",value:u.filter((n)=>n.providerId==="D601").length,hint:"compute-node docker"}),$(cl,{label:"集成前端",value:u.filter((n)=>n.frontend?.integrated).length,hint:"UniDesk React 页面"}))),$(du,{title:"服务映射",eyebrow:"Repo Reference + Runtime"},u.length===0?$(jl,{title:"暂无用户服务",text:"在 config.json 的 microservices 中登记用户服务的 provider、仓库引用和后端映射"}):$("div",{className:"table-wrap"},$("table",{className:"microservice-table"},$("thead",null,$("tr",null,$("th",null,"服务"),$("th",null,"Provider"),$("th",null,"代码引用"),$("th",null,"Docker 引用"),$("th",null,"后端映射"),$("th",null,"开发入口"),$("th",null,"运行态"),$("th",null,"操作"))),$("tbody",null,u.map((n)=>{let i=ZL(n),y=Wp(n),t=_L(n),_=i.availability||{},c=_.status||(i.providerStatus==="online"?"unknown":"unhealthy");return $("tr",{key:n.id,"data-testid":`microservice-row-${ef(n.id)}`},$("td",null,$("strong",null,n.name),$("code",null,n.id)),$("td",null,$("strong",null,i.providerName||n.providerId),$("code",null,n.providerId)),$("td",null,$("span",null,y.url||"--"),$("code",null,y.commitId||"--")),$("td",null,$("span",null,y.composeFile||"--"),$("code",null,`${y.composeService||"--"} / ${y.containerName||"--"}`)),$("td",null,$(wl,{status:t.public?"warn":"online"},t.public?"public":"private"),$("code",null,`${t.nodeBindHost||"--"}:${t.nodePort||"--"} -> ${t.proxyMode||"--"}`)),$("td",null,$("span",null,n.development?.sshPassthrough?"SSH 透传":"未配置"),$("code",null,n.development?.worktreePath||"--")),$("td",null,$(wl,{status:c==="healthy"?"online":c==="unknown"?"warn":"failed"},c),$("span",null,_.reason||i.providerStatus||"unknown"),$(sn,{data:i.container,empty:"容器快照未上报"})),$("td",null,$("div",{className:"microservice-actions"},n.id==="findjob"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","findjob"),"data-testid":"open-findjob-button"},"打开"):null,n.id==="pipeline"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","pipeline"),"data-testid":"open-pipeline-button"},"打开"):null,n.id==="todo-note"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","todo-note"),"data-testid":"open-todo-note-button"},"打开"):null,n.id==="met-nonlinear"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","met-nonlinear"),"data-testid":"open-met-nonlinear-button"},"打开"):null,n.id==="claudeqq"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","claudeqq"),"data-testid":"open-claudeqq-button"},"打开"):null,n.id==="baidu-netdisk"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","baidu-netdisk"),"data-testid":"open-baidu-netdisk-button"},"打开"):null,n.id==="oa-event-flow"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","oa-event-flow"),"data-testid":"open-oa-event-flow-button"},"打开"):null,n.id==="k3sctl-adapter"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","k3sctl"),"data-testid":"open-k3sctl-button"},"打开"):null,n.id==="code-queue"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","code-queue"),"data-testid":"open-code-queue-button"},"打开"):null,n.id==="project-manager"?$("button",{type:"button",className:"ghost-btn",onClick:()=>f("apps","project-manager"),"data-testid":"open-project-manager-button"},"打开"):null,$(bl,{title:`用户服务 ${n.id}`,data:n,onOpen:l}))))}))))))}function ap({nodes:u,onDispatched:l,onRaw:f}){let r=u.filter((V)=>V.status==="online"),[n,i]=bu(r[0]?.providerId||u[0]?.providerId||""),[y,t]=bu("docker.ps"),[_,c]=bu("frontend"),[A,j]=bu("operator-check"),[F,J]=bu("normal"),[Q,w]=bu(!1),[L,U]=bu(""),[N,q]=bu(!1),[W,z]=bu(null),[Z,H]=bu("");J0(()=>{if(!n&&(r[0]?.providerId||u[0]?.providerId))i(r[0]?.providerId||u[0].providerId)},[u.length,r.length,n]);function E(){return{source:_,note:A,priority:F}}function D(){U(JSON.stringify(E(),null,2)),w(!0)}async function h(V){V.preventDefault(),q(!0),H("");try{let S=Q?JSON.parse(L||"{}"):E(),p=await Tu(`${gu.apiBaseUrl}/dispatch`,{method:"POST",body:JSON.stringify({providerId:n,command:y,payload:S})});z(p),await l()}catch(S){H(Ou(S,"下发失败"))}finally{q(!1)}}return $("div",{className:"page-grid dispatch-grid"},$(du,{title:"下发任务",eyebrow:"Real WebSocket Dispatch"},$("form",{className:"dispatch-form",onSubmit:h},$("label",null,"Provider",$("select",{value:n,onChange:(V)=>i(V.target.value)},u.map((V)=>$("option",{key:V.providerId,value:V.providerId},`${V.name} / ${V.providerId}`)))),$("label",null,"Command",$("select",{value:y,onChange:(V)=>t(V.target.value)},$("option",{value:"docker.ps"},"docker.ps"),$("option",{value:"host.ssh"},"host.ssh"),$("option",{value:"microservice.http"},"microservice.http"),$("option",{value:"echo"},"echo"))),$("label",null,"来源",$("input",{value:_,onChange:(V)=>c(V.target.value)})),$("label",null,"备注",$("input",{value:A,onChange:(V)=>j(V.target.value)})),$("label",null,"优先级",$("select",{value:F,onChange:(V)=>J(V.target.value)},$("option",{value:"normal"},"normal"),$("option",{value:"low"},"low"),$("option",{value:"urgent"},"urgent"))),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",onClick:D},"查看原始JSON"),$("button",{type:"submit",disabled:N||!n},N?"下发中":"下发任务")),Q?$("label",{className:"raw-editor-label"},"高级 Payload",$("textarea",{className:"raw-editor",value:L,onChange:(V)=>U(V.target.value)})):null,$(il,{error:Z,wide:!0}))),$(du,{title:"下发结果",eyebrow:"Response"},W?$("div",{className:"result-card"},$(wl,{status:W.status||"queued"},W.status||"queued"),$("dl",null,$("dt",null,"Task ID"),$("dd",null,$("code",null,W.taskId||"--")),$("dt",null,"Provider 在线"),$("dd",null,hi(W.providerOnline))),$(bl,{title:"Dispatch Response",data:W,onOpen:f})):$(jl,{title:"等待操作",text:"任务响应会以结构化结果卡展示"})))}function FL({task:u,onRaw:l}){return $("article",{className:"compact-row"},$(wl,{status:u.status}),$("div",null,$("strong",null,u.command),$("code",null,u.id)),$("span",null,Ly(u)?`已等待 ${i7(u.updatedAt)}`:`耗时 ${b0(NL(u)??0)}`),$(bl,{title:`Task ${u.id}`,data:v_(u),onOpen:l}))}function DL({task:u}){let l=NL(u),f=Ly(u);return $("div",{className:"task-duration"},$("strong",null,l===null?"--":b0(l)),$("span",null,f?`已运行 / 创建 ${qu(u.createdAt)}`:`创建 ${qu(u.createdAt)}`))}function op({task:u}){let l=String(u?.status||"").toLowerCase(),f=u?.result,r=f&&typeof f==="object"&&!Array.isArray(f)?f:{},i=["exitCode","code","signal","timeoutMs","previousStatus","mode"].filter((y)=>r[y]!==void 0&&r[y]!==null);if(l==="failed"){let y=qL(u);return $("div",{className:"task-diagnostic failed"},$("b",null,"失败原因"),$("span",{className:"diagnostic-reason"},hi(y)),i.length>0?$("div",{className:"diagnostic-meta"},i.map((t)=>$("span",{key:t,className:"data-chip"},$("b",null,t),$("span",null,hi(r[t]))))):null)}if(Ly(u))return $("div",{className:"task-diagnostic warn"},$("b",null,"等待终态"),$("span",null,`最后更新 ${i7(u.updatedAt)} 前`));return $("div",{className:"task-diagnostic ok"},$("b",null,"完成摘要"),$(sn,{data:f,empty:"无执行输出"}))}function dp({tasks:u,onRaw:l}){let f=u.filter(Ly);return $("div",{"data-testid":"pending-task-page"},$(du,{title:"待处理任务",eyebrow:`${f.length} Pending`},f.length===0?$(jl,{title:"当前无待处理任务",text:"queued / dispatched / running 会在超时后自动转为 failed;历史记录仍可在任务历史中查看"}):$("div",{className:"table-wrap","data-testid":"pending-task-table"},$("table",null,$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"Provider"),$("th",null,"已等待"),$("th",null,"载荷摘要"),$("th",null,"操作"))),$("tbody",null,f.map((r)=>$("tr",{key:r.id},$("td",null,$(wl,{status:r.status})),$("td",null,$("strong",null,r.command),$("code",null,r.id)),$("td",null,$("code",null,r.providerId)),$("td",null,i7(r.updatedAt)),$("td",null,$(sn,{data:r.payload})),$("td",null,$(bl,{title:`Pending Task ${r.id}`,data:v_(r),onOpen:l})))))))))}function ep({tasks:u,onRaw:l}){return $("div",{"data-testid":"task-history-page"},$(du,{title:"任务历史",eyebrow:`${u.length} Tasks`},u.length===0?$(jl,{title:"暂无任务",text:"下发任务后会在这里看到生命周期"}):$("div",{className:"table-wrap"},$("table",{className:"task-history-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"Provider"),$("th",null,"任务耗时"),$("th",null,"载荷摘要"),$("th",null,"诊断信息"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,u.map((f)=>$("tr",{key:f.id,"data-testid":`task-row-${ef(f.id)}`},$("td",null,$(wl,{status:f.status})),$("td",null,$("strong",null,f.command),$("code",null,f.id)),$("td",null,$("code",null,f.providerId)),$("td",null,$(DL,{task:f})),$("td",null,$(sn,{data:f.payload})),$("td",null,$(op,{task:f})),$("td",null,qu(f.updatedAt)),$("td",null,$(bl,{title:`Task ${f.id}`,data:v_(f),onOpen:l})))))))))}function um({tasks:u,onRaw:l}){let f=u.filter((r)=>["succeeded","failed"].includes(r.status));return $(du,{title:"执行结果",eyebrow:"Finished Tasks"},f.length===0?$(jl,{title:"暂无结果",text:"任务完成后展示 provider 返回的结构化摘要"}):$("div",{className:"result-grid"},f.map((r)=>$("article",{key:r.id,className:"result-card"},$("div",{className:"node-card-head"},$("strong",null,r.command),$(wl,{status:r.status})),$("code",null,r.id),$(sn,{data:r.result,empty:"无执行输出"}),$(bl,{title:`Task Result ${r.id}`,data:v_(r),onOpen:l})))))}function lm(u){if(!u||typeof u!=="object")return"--";if(u.type==="interval")return`每 ${b0(Number(u.everySeconds||0))}`;return`每天 ${u.timeOfDay||"03:00"} UTC`}function fm(u){if(!u||typeof u!=="object")return"--";if(u.type==="pgdata_backup")return`PGDATA -> ${u.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA"}`;if(u.type==="dispatch")return`${u.providerId||"--"} / ${u.command||"--"}`;return String(u.type||"--")}function rm(u){let l=String(u||"").toLowerCase();if(l==="succeeded")return"online";if(l==="failed")return"failed";if(l==="running"||l==="queued")return"warn";return l}function nm(u){let l=Number(u?.durationMs);if(Number.isFinite(l)&&l>=0)return b0(l/1000);let f=Q0(u?.startedAt||u?.createdAt);if(f===null)return"--";let n=Q0(u?.finishedAt)??Date.now();return b0(Math.max(0,(n-f)/1000))}function UL(u){return{id:"unidesk-pgdata-baidu-daily",name:"PGDATA daily Baidu Netdisk backup",description:"Daily PostgreSQL physical base backup uploaded to Baidu Netdisk /SERVER_DATA with monthly rotation.",enabled:!0,timeOfDay:"03:30",actionType:"pgdata_backup",providerId:u[0]?.providerId||"main-server",command:"echo",payloadJson:JSON.stringify({source:"scheduled-task",message:"hello from scheduler"},null,2),remoteBaseDir:"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:"server-data/unidesk-pg-data",timeoutMs:"3600000"}}function im({schedules:u,scheduleRuns:l,nodes:f,refresh:r,onRaw:n}){let[i,y]=bu(UL(f||[])),[t,_]=bu(!1),[c,A]=bu(""),[j,F]=bu(""),J=[...l||[]].sort((W,z)=>(Q0(z.updatedAt)??0)-(Q0(W.updatedAt)??0));function Q(W,z){y((Z)=>({...Z,[W]:z}))}function w(W){let z=W?.action||{};y({id:W?.id||"",name:W?.name||"",description:W?.description||"",enabled:W?.enabled!==!1,timeOfDay:W?.schedule?.timeOfDay||"03:30",actionType:z.type||"dispatch",providerId:z.providerId||f[0]?.providerId||"main-server",command:z.command||"echo",payloadJson:JSON.stringify(z.payload||{source:"scheduled-task"},null,2),remoteBaseDir:z.remoteBaseDir||"/SERVER_DATA/UNIDESK_PG_DATA",stagingSubdir:z.stagingSubdir||"server-data/unidesk-pg-data",timeoutMs:String(z.timeoutMs||3600000)}),F(`正在编辑 ${W?.id||""}`)}function L(){let W={id:i.id,name:i.name,description:i.description,enabled:i.enabled,concurrencyPolicy:"skip",schedule:{type:"daily",timeOfDay:i.timeOfDay,timezone:"Etc/UTC"}};if(i.actionType==="pgdata_backup")return{...W,action:{type:"pgdata_backup",volumeName:"unidesk_pgdata_10gb",remoteBaseDir:i.remoteBaseDir,stagingSubdir:i.stagingSubdir,timeoutMs:Number(i.timeoutMs)||3600000,cleanupLocal:!0}};return{...W,action:{type:"dispatch",providerId:i.providerId,command:i.command,payload:JSON.parse(i.payloadJson||"{}"),timeoutMs:Number(i.timeoutMs)||600000}}}async function U(W){W.preventDefault(),_(!0),A(""),F("");try{let z=L(),Z=encodeURIComponent(String(z.id));await Tu(`${gu.apiBaseUrl}/schedules/${Z}`,{method:"PUT",body:JSON.stringify(z)}),F("定时任务已保存"),await r()}catch(z){A(Ou(z,"保存定时任务失败"))}finally{_(!1)}}async function N(W){if(!W?.id)return;_(!0),A(""),F("");try{await Tu(`${gu.apiBaseUrl}/schedules/${encodeURIComponent(W.id)}`,{method:"DELETE"}),F(`已删除 ${W.id}`),await r()}catch(z){A(Ou(z,"删除定时任务失败"))}finally{_(!1)}}async function q(W){if(!W?.id)return;_(!0),A(""),F("");try{let z=await Tu(`${gu.apiBaseUrl}/schedules/${encodeURIComponent(W.id)}/run`,{method:"POST",body:"{}"});F(`已触发 ${W.id} / ${z?.run?.id||"run"}`),await r()}catch(z){A(Ou(z,"触发定时任务失败"))}finally{_(!1)}}return $("div",{className:"page-grid scheduled-task-page","data-testid":"scheduled-task-page"},$(du,{title:"定时任务",eyebrow:`${(u||[]).length} Schedules`},(u||[]).length===0?$(jl,{title:"暂无定时任务",text:"创建 daily / dispatch / PGDATA backup 任务后会在这里展示下一次执行时间和最近结果"}):$("div",{className:"schedule-card-grid"},(u||[]).map((W)=>$("article",{key:W.id,className:"schedule-card","data-testid":`schedule-row-${ef(W.id)}`},$("div",{className:"node-card-head"},$("strong",null,W.name||W.id),$(wl,{status:W.enabled?"online":"warn"},W.enabled?"enabled":"disabled")),$("code",null,W.id),$("dl",null,$("dt",null,"计划"),$("dd",null,lm(W.schedule)),$("dt",null,"动作"),$("dd",null,fm(W.action)),$("dt",null,"下次执行"),$("dd",null,qu(W.nextRunAt)),$("dt",null,"最近执行"),$("dd",null,W.lastRunAt?`${qu(W.lastRunAt)} / ${W.lastRunId||"--"}`:"--")),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>w(W)},"编辑"),$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>q(W),"data-testid":`schedule-run-${ef(W.id)}`},"手动触发"),$("button",{type:"button",className:"ghost-btn danger",disabled:t,onClick:()=>N(W)},"删除"),$(bl,{title:`Schedule ${W.id}`,data:W,onOpen:n})))))),$(du,{title:i.id?"配置定时任务":"新建定时任务",eyebrow:"CRUD"},$("form",{className:"dispatch-form schedule-form",onSubmit:U},$("label",null,"ID",$("input",{value:i.id,onChange:(W)=>Q("id",W.target.value)})),$("label",null,"名称",$("input",{value:i.name,onChange:(W)=>Q("name",W.target.value)})),$("label",null,"每日执行时间 UTC",$("input",{value:i.timeOfDay,placeholder:"03:30",onChange:(W)=>Q("timeOfDay",W.target.value)})),$("label",null,"启用",$("select",{value:i.enabled?"true":"false",onChange:(W)=>Q("enabled",W.target.value==="true")},$("option",{value:"true"},"enabled"),$("option",{value:"false"},"disabled"))),$("label",null,"动作类型",$("select",{value:i.actionType,onChange:(W)=>Q("actionType",W.target.value)},$("option",{value:"pgdata_backup"},"PGDATA 备份到百度网盘"),$("option",{value:"dispatch"},"Provider Dispatch"))),i.actionType==="pgdata_backup"?[$("label",{key:"remote"},"网盘根目录",$("input",{value:i.remoteBaseDir,onChange:(W)=>Q("remoteBaseDir",W.target.value)})),$("label",{key:"staging"},"本地 staging 子目录",$("input",{value:i.stagingSubdir,onChange:(W)=>Q("stagingSubdir",W.target.value)}))]:[$("label",{key:"provider"},"Provider",$("select",{value:i.providerId,onChange:(W)=>Q("providerId",W.target.value)},(f||[]).map((W)=>$("option",{key:W.providerId,value:W.providerId},`${W.name} / ${W.providerId}`)))),$("label",{key:"command"},"Command",$("select",{value:i.command,onChange:(W)=>Q("command",W.target.value)},$("option",{value:"echo"},"echo"),$("option",{value:"docker.ps"},"docker.ps"),$("option",{value:"host.ssh"},"host.ssh"),$("option",{value:"microservice.http"},"microservice.http"))),$("label",{key:"payload",className:"raw-editor-label"},"Payload JSON",$("textarea",{className:"raw-editor",value:i.payloadJson,onChange:(W)=>Q("payloadJson",W.target.value)}))],$("label",null,"超时 ms",$("input",{value:i.timeoutMs,onChange:(W)=>Q("timeoutMs",W.target.value)})),$("label",{className:"raw-editor-label"},"描述",$("textarea",{className:"raw-editor compact",value:i.description,onChange:(W)=>Q("description",W.target.value)})),$("div",{className:"dispatch-actions"},$("button",{type:"button",className:"ghost-btn",disabled:t,onClick:()=>y(UL(f||[]))},"重置"),$("button",{type:"submit",disabled:t||!i.id},t?"保存中":"保存任务")),j?$("p",{className:"muted paragraph"},j):null,$(il,{error:c,wide:!0}))),$(du,{title:"历史执行记录",eyebrow:`${J.length} Runs`},J.length===0?$(jl,{title:"暂无执行记录",text:"定时触发或手动触发后会生成 run history"}):$("div",{className:"table-wrap"},$("table",{className:"task-history-table schedule-run-table"},$("thead",null,$("tr",null,$("th",null,"状态"),$("th",null,"任务"),$("th",null,"触发"),$("th",null,"耗时"),$("th",null,"结果摘要"),$("th",null,"更新时间"),$("th",null,"操作"))),$("tbody",null,J.map((W)=>$("tr",{key:W.id,"data-testid":`schedule-run-row-${ef(W.id)}`},$("td",null,$(wl,{status:rm(W.status)},W.status)),$("td",null,$("strong",null,W.scheduleId),$("code",null,W.id),W.taskId?$("code",null,W.taskId):null),$("td",null,W.trigger||"--"),$("td",null,nm(W)),$("td",null,$(sn,{data:W.result||W.error,empty:"无结果"})),$("td",null,qu(W.updatedAt)),$("td",null,$(bl,{title:`Schedule Run ${W.id}`,data:W,onOpen:n})))))))))}function ym({data:u}){let l=u.overview||{};return $("div",{className:"page-grid topology-grid"},$(du,{title:"公开入口",eyebrow:"Public"},$("div",{className:"endpoint-list"},$("article",null,$("b",null,"Frontend"),$("span",null,gu.frontendPublicUrl||window.location.origin),$(wl,{status:"online"},"public")),$("article",null,$("b",null,"Provider Ingress"),$("span",null,gu.providerIngressPublicUrl||"ws://public/ws/provider"),$(wl,{status:"online"},"public")))),$(du,{title:"内部服务",eyebrow:"Docker Network Only"},$("div",{className:"endpoint-list"},$("article",null,$("b",null,"backend-core API"),$("span",null,"http://backend-core:8080"),$(wl,{status:"internal"},"internal")),$("article",null,$("b",null,"database"),$("span",null,"postgres://database:5432/unidesk"),$(wl,{status:"internal"},"internal")))),$(du,{title:"运行态",eyebrow:"Runtime"},$("div",{className:"metric-grid"},$(cl,{label:"DB Ready",value:l.dbReady?"YES":"NO",hint:"internal health"}),$(cl,{label:"Online Nodes",value:l.onlineNodeCount??0,hint:"provider-gateway self-link"}))))}function tm({session:u}){return $(du,{title:"认证策略",eyebrow:"Frontend Login"},$("div",{className:"policy-grid"},$("article",null,$("span",null,"默认账号"),$("strong",null,gu.authUsername||"admin")),$("article",null,$("span",null,"当前会话"),$("strong",null,u?.user?.username||"--")),$("article",null,$("span",null,"Session TTL"),$("strong",null,`${gu.sessionTtlSeconds||0}s`)),$("article",null,$("span",null,"API 访问"),$("strong",null,"同源 Cookie 保护"))),$("p",{className:"muted paragraph"},"浏览器只访问 frontend 同源接口;frontend 容器使用 Docker 内网代理 backend-core API。"))}function _m(){return $(du,{title:"安全边界",eyebrow:"Exposure Rule"},$("div",{className:"security-board"},$("article",{className:"allow"},$("b",null,"允许公网"),$("span",null,"frontend 登录入口"),$("span",null,"provider ingress WebSocket/health")),$("article",{className:"deny"},$("b",null,"禁止公网"),$("span",null,"backend-core REST API"),$("span",null,"PostgreSQL database")),$("article",null,$("b",null,"数据库卷"),$("span",null,"named volume unidesk_pgdata_10gb"),$("span",null,"CLI stop/start 不删除数据卷"))))}function $m({activeModule:u,activeTab:l,data:f,session:r,refresh:n,onRaw:i,onNavigate:y}){if(u==="ops"&&l==="status")return $(Ep,{data:f,onRaw:i,onNavigate:y});if(u==="ops"&&l==="performance")return $(Rp,{onRaw:i});if(u==="ops"&&l==="events")return $(Op,{events:f.events,onRaw:i});if(u==="ops"&&l==="logs")return $(Hp,{logs:f.logs,onRaw:i});if(u==="nodes"&&l==="list")return $(Bp,{nodes:f.nodes,onRaw:i});if(u==="nodes"&&l==="monitor")return $(Xp,{nodes:f.nodes,systemStatuses:f.systemStatuses,tasks:f.tasks,onRaw:i,refresh:n});if(u==="nodes"&&l==="docker")return $(Ip,{nodes:f.nodes,dockerStatuses:f.dockerStatuses,onRaw:i});if(u==="nodes"&&l==="gateway")return $(bp,{nodes:f.nodes,tasks:f.tasks,onRaw:i});if(u==="nodes"&&l==="labels")return $(Vp,{nodes:f.nodes});if(u==="nodes"&&l==="heartbeats")return $(Dp,{nodes:f.nodes});if(u==="tasks"&&l==="dispatch")return $(ap,{nodes:f.nodes,onDispatched:n,onRaw:i});if(u==="tasks"&&l==="scheduled")return $(im,{schedules:f.schedules,scheduleRuns:f.scheduleRuns,nodes:f.nodes,refresh:n,onRaw:i});if(u==="tasks"&&l==="pending")return $(dp,{tasks:f.pendingTasks,onRaw:i});if(u==="tasks"&&l==="history")return $(ep,{tasks:f.tasks,onRaw:i});if(u==="tasks"&&l==="results")return $(um,{tasks:f.tasks,onRaw:i});if(u==="apps"&&l==="catalog")return $(sp,{microservices:f.microservices,onRaw:i,onNavigate:y});if(u==="apps"&&l==="todo-note")return $(aw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="findjob")return $(hQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="pipeline")return $(xw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="met-nonlinear")return $(gQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="claudeqq")return $(qJ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="baidu-netdisk")return $(JJ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="filebrowser")return $(xQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="oa-event-flow")return $(lN,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="apps"&&l==="k3sctl")return $(fL,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl,onNavigate:y});if(u==="apps"&&l==="code-queue")return $(pQ,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl,initialTasksData:_p});if(u==="apps"&&l==="project-manager")return $(vw,{microservices:f.microservices,onRaw:i,apiBaseUrl:gu.apiBaseUrl});if(u==="config"&&l==="topology")return $(ym,{data:f});if(u==="config"&&l==="auth")return $(tm,{session:r});if(u==="config"&&l==="security")return $(_m);return $(jl,{title:"未找到页面",text:"请选择左侧主模块和顶部子功能标签"})}function cm({session:u,onLogout:l}){let f=j2(Dr,window.location.pathname),[r,n]=bu(f.moduleId),[i,y]=bu({...st,[f.moduleId]:f.tabId}),[t,_]=bu({overview:null,nodes:[],systemStatuses:[],dockerStatuses:[],microservices:[],events:[],tasks:[],pendingTasks:[],schedules:[],scheduleRuns:[],logs:[]}),[c,A]=bu({ok:!1,text:"连接中"}),[j,F]=bu(null),[J,Q]=bu(new Date),[w,L]=bu(null),[U,N]=bu(!1),[q,W]=bu(!1),z=gn.default.useRef(!1),Z=Dr.moduleById[r]||Dr.modules[0],H=i[r]||st[r]||Z.tabs[0].id,E=Array.isArray(t.microservices)?t.microservices:[],D=E.length===0&&r==="apps"&&H==="code-queue"?[$p]:E,h=D===E?t:{...t,microservices:D},V=r==="apps"?D.find((o)=>String(o?.id||"")===(H==="k3sctl"?"k3sctl-adapter":H)):null,S=V?ZL(V):{},p=Z.tabs.find((o)=>o.id===H)?.label||H,O=V?[{key:"microservice",label:"用户服务",value:`${p} ${S.providerStatus==="online"?"在线":S.providerStatus||"未知"}`,tone:S.providerStatus==="online"?"ok":"warn",testId:"active-microservice-status"}]:[];async function m(){if(z.current)return;z.current=!0,W(!0);try{let o=[],g=(ju,zu)=>{o.push([ju,Tu(zu)])},x=r==="ops"&&H==="status",lu=x||r==="config"&&H==="topology",_u=x||r==="nodes"||r==="tasks"&&(H==="dispatch"||H==="scheduled"),$u=r==="apps"&&H!=="code-queue";if(lu)g("overview",`${gu.apiBaseUrl}/overview`);if(_u)g("nodes",`${gu.apiBaseUrl}/nodes`);if(r==="nodes"&&H==="monitor")g("systemStatuses",`${gu.apiBaseUrl}/nodes/system-status?limit=60`),g("tasks",`${gu.apiBaseUrl}/tasks?limit=120&summary=1`);else if(r==="nodes"&&H==="docker")g("dockerStatuses",`${gu.apiBaseUrl}/nodes/docker-status`);else if(r==="nodes"&&H==="gateway")g("tasks",`${gu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(r==="tasks"&&H==="scheduled")g("schedules",`${gu.apiBaseUrl}/schedules?limit=100`),g("scheduleRuns",`${gu.apiBaseUrl}/schedules/runs?limit=100`);else if(r==="tasks"&&H==="pending")g("pendingTasks",`${gu.apiBaseUrl}/tasks?status=pending&limit=100&summary=1`);else if(r==="tasks"&&(H==="history"||H==="results"))g("tasks",`${gu.apiBaseUrl}/tasks?limit=300&summary=1`);else if(x)g("tasks",`${gu.apiBaseUrl}/tasks?limit=8&lite=1`),g("pendingTasks",`${gu.apiBaseUrl}/tasks?status=pending&limit=20&lite=1`);if($u)g("microservices",`${gu.apiBaseUrl}/microservices`);if(r==="ops"&&H==="events")g("events",`${gu.apiBaseUrl}/events?limit=100`);if(r==="ops"&&H==="logs")g("logs","/logs?limit=100");await Promise.all(o.map(async([ju,zu])=>{let Wu=await zu,P={};if(ju==="overview")P.overview=Wu;if(ju==="nodes")P.nodes=Wu.nodes||[];if(ju==="systemStatuses")P.systemStatuses=Wu.systemStatuses||[];if(ju==="dockerStatuses")P.dockerStatuses=Wu.dockerStatuses||[];if(ju==="microservices")P.microservices=Wu.microservices||[];if(ju==="events")P.events=Wu.events||[];if(ju==="tasks")P.tasks=Wu.tasks||[];if(ju==="pendingTasks")P.pendingTasks=Wu.tasks||[];if(ju==="schedules")P.schedules=Wu.schedules||[];if(ju==="scheduleRuns")P.scheduleRuns=Wu.runs||[];if(ju==="logs")P.logs=Wu.logs||[];_((e)=>({...e,...P}))})),A({ok:!0,text:"核心在线"}),F(new Date)}catch(o){if(A({ok:!1,text:Ou(o,"连接失败")}),o.status===401)l(!1)}finally{z.current=!1,W(!1)}}J0(()=>{let o=()=>{if(!iL())return;m()};o();let g=setInterval(o,cp(r,H)),x=()=>{if(iL())o()};return document.addEventListener("visibilitychange",x),()=>{clearInterval(g),document.removeEventListener("visibilitychange",x)}},[r,H]),J0(()=>{let o=setInterval(()=>Q(new Date),1000);return()=>clearInterval(o)},[]),J0(()=>{let o=dQ(Dr,window.location.pathname);if(o&&window.location.pathname!==o)window.history.replaceState(null,"",o)},[]),J0(()=>{let o=()=>{let g=j2(Dr,window.location.pathname);n(g.moduleId),y((x)=>({...x,[g.moduleId]:g.tabId})),L(null)};return window.addEventListener("popstate",o),()=>window.removeEventListener("popstate",o)},[]),J0(()=>{window.scrollTo({top:0,left:0,behavior:"auto"})},[r,H]);function X(o,g,x="push"){let lu=Dr.moduleById[o]?o:Dr.fallbackTarget.moduleId,_u=Dr.moduleById[lu]?.tabs.some((ju)=>ju.id===g)?g:st[lu]||Dr.moduleById[lu]?.tabs[0]?.id||Dr.fallbackTarget.tabId;n(lu),y((ju)=>({...ju,[lu]:_u}));let $u=yc(Dr,lu,_u);if(window.location.pathname!==$u){let ju=x==="replace"?"replaceState":"pushState";window.history[ju](null,"",$u)}}function v(o,g){L({title:o,data:g})}let[T,Y]=bu(!1),{unreadCount:k,notifications:I}=Xf(),b=I.length>0?I[I.length-1]:null;return $("div",{className:`shell ${U?"rail-collapsed":""}`,"data-testid":"app-shell"},$(zp,{activeModule:r,activeTabs:i,onNavigate:X,collapsed:U,onToggle:()=>N((o)=>!o)}),$("main",{className:"workspace"},$(Kp,{connection:c,lastRefresh:j,onRefresh:m,onLogout:()=>l(!0),session:u,clock:J,activeStatusItems:O,onNotificationToggle:()=>Y((o)=>!o),unreadCount:k}),$(Tp,{module:Z,activeTab:H,onNavigate:X}),$(n7.Provider,{value:q},$($m,{activeModule:r,activeTab:H,data:h,session:u,refresh:m,onRaw:v,onNavigate:X}))),$(wp,{raw:w,onClose:()=>L(null)}),b&&$(nL,{key:b.id,notification:b}),T&&$(rL,{onClose:()=>Y(!1)}))}function Am(){let[u,l]=bu(!0),[f,r]=bu(null);async function n(){l(!0);try{let y=await Tu("/api/session");r(y.authenticated?y:null)}catch{r(null)}finally{l(!1)}}async function i(y){if(y)try{await Tu("/logout",{method:"POST"})}catch{}r(null)}if(J0(()=>{n()},[]),u)return $("main",{className:"loading-screen"},$("div",{className:"brand-mark"},"UD"),$("span",null,"加载会话"));if(!f)return $(Lp,{onLogin:r});return $(UJ,null,$(cm,{session:f,onLogout:i}))}var XL=document.getElementById("root");if(XL===null)throw Error("root element not found");JL.createRoot(XL).render($(Am));})(); diff --git a/src/components/frontend/public/style.css b/src/components/frontend/public/style.css index 374a9ce1..532592c6 100644 --- a/src/components/frontend/public/style.css +++ b/src/components/frontend/public/style.css @@ -6291,11 +6291,11 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } background: rgba(255,255,255,0.3); } -.v3s-page { +.k3s-page { display: grid; gap: 14px; } -.v3s-hero-panel { +.k3s-hero-panel { position: relative; overflow: hidden; border-color: rgba(215, 161, 58, 0.34); @@ -6304,14 +6304,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } radial-gradient(circle at 12% 84%, rgba(78, 183, 168, 0.13), transparent 34%), linear-gradient(135deg, rgba(255,255,255,0.045), rgba(255,255,255,0.012)); } -.v3s-hero { +.k3s-hero { display: grid; grid-template-columns: 128px minmax(0, 1fr); align-items: center; gap: 18px; margin-bottom: 16px; } -.v3s-orb { +.k3s-orb { position: relative; display: grid; place-items: center; @@ -6328,20 +6328,20 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } color: var(--accent); font-weight: 800; } -.v3s-orb::before, -.v3s-orb::after { +.k3s-orb::before, +.k3s-orb::after { content: ""; position: absolute; inset: 12px; border: 1px dashed rgba(78,183,168,0.38); border-radius: inherit; } -.v3s-orb::after { +.k3s-orb::after { inset: 28px; border-style: solid; border-color: rgba(255,255,255,0.12); } -.v3s-hero-copy h2 { +.k3s-hero-copy h2 { max-width: 860px; margin: 0 0 8px; color: var(--text); @@ -6350,14 +6350,14 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } letter-spacing: 0.08em; text-transform: none; } -.v3s-route-strip { +.k3s-route-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; } -.v3s-route-strip span { +.k3s-route-strip span { padding: 5px 8px; border: 1px solid rgba(215,161,58,0.5); border-radius: 999px; @@ -6367,7 +6367,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } font-weight: 800; letter-spacing: 0.16em; } -.v3s-route-strip code { +.k3s-route-strip code { max-width: 100%; padding: 5px 8px; border: 1px solid var(--line-soft); @@ -6377,79 +6377,79 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } overflow-wrap: anywhere; white-space: normal; } -.v3s-control-plane-grid { +.k3s-control-plane-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 12px; } -.v3s-control-plane-card { +.k3s-control-plane-card { min-height: 96px; padding: 12px; border: 1px solid rgba(255,255,255,0.1); background: linear-gradient(135deg, rgba(0,0,0,0.22), rgba(78,183,168,0.07)); } -.v3s-control-plane-card span { +.k3s-control-plane-card span { color: var(--muted); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; } -.v3s-control-plane-card strong { +.k3s-control-plane-card strong { display: block; margin-top: 8px; color: var(--text); font-size: 17px; letter-spacing: 0.08em; } -.v3s-control-plane-card p { +.k3s-control-plane-card p { margin: 8px 0 0; color: var(--muted); font-size: 12px; line-height: 1.45; overflow-wrap: anywhere; } -.v3s-service-panel { +.k3s-service-panel { border-color: rgba(78,183,168,0.25); } -.v3s-service-summary { +.k3s-service-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 12px; } -.v3s-service-summary > div, -.v3s-instance-card { +.k3s-service-summary > div, +.k3s-instance-card { border: 1px solid var(--line-soft); background: rgba(0,0,0,0.18); } -.v3s-service-summary > div { +.k3s-service-summary > div { display: grid; gap: 6px; min-height: 66px; padding: 10px; } -.v3s-service-summary span, -.v3s-kv dt { +.k3s-service-summary span, +.k3s-kv dt { color: var(--muted); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; } -.v3s-service-summary strong { +.k3s-service-summary strong { color: var(--text); font-size: 18px; } -.v3s-instance-grid { +.k3s-instance-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; } -.v3s-instance-card { +.k3s-instance-card { padding: 12px; border-left: 3px solid rgba(78,183,168,0.7); } -.v3s-instance-role { +.k3s-instance-role { display: flex; align-items: center; justify-content: space-between; @@ -6459,34 +6459,34 @@ input:focus, select:focus, textarea:focus { border-color: var(--accent-2); } font-size: 11px; letter-spacing: 0.15em; } -.v3s-kv { +.k3s-kv { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 6px 10px; margin: 0; } -.v3s-kv dd { +.k3s-kv dd { min-width: 0; margin: 0; color: var(--text); overflow-wrap: anywhere; } -.v3s-kv code { +.k3s-kv code { white-space: normal; } @media (max-width: 780px) { - .v3s-hero { + .k3s-hero { grid-template-columns: 1fr; } - .v3s-orb { + .k3s-orb { width: 96px; height: 96px; } - .v3s-service-summary { + .k3s-service-summary { grid-template-columns: 1fr 1fr; } - .v3s-control-plane-grid { + .k3s-control-plane-grid { grid-template-columns: 1fr; } } diff --git a/src/components/frontend/src/app.tsx b/src/components/frontend/src/app.tsx index 9d44fae2..6c4fe61d 100644 --- a/src/components/frontend/src/app.tsx +++ b/src/components/frontend/src/app.tsx @@ -14,7 +14,7 @@ import { PipelinePage } from "./pipeline"; import { ProjectManagerPage } from "./project-manager"; import { TodoNotePage } from "./todo-note"; import { TopStatusBar } from "./top-status"; -import { V3sCtlPage } from "./v3sctl"; +import { K3sCtlPage } from "./k3sctl"; import { LoadingTitle } from "./loading-indicator"; import { errorMessage, requestJson } from "./unidesk-error"; import { UniDeskErrorBanner } from "./unidesk-error-banner"; @@ -46,21 +46,21 @@ const fastCodeQueueService = { name: "Code Queue", providerId: "D601", description: "Code Queue", - repository: { containerName: "v3s:code-queue" }, + repository: { containerName: "k3s:code-queue" }, backend: { - nodeBaseUrl: "v3s://code-queue", - nodeBindHost: "v3s://unidesk/code-queue", + nodeBaseUrl: "k3s://code-queue", + nodeBindHost: "k3s://unidesk/code-queue", nodePort: 4222, - proxyMode: "v3sctl-adapter-http", + proxyMode: "k3sctl-adapter-http", public: false, }, deployment: { - mode: "v3sctl-managed", - adapterServiceId: "v3sctl-adapter", - v3sServiceId: "code-queue", + mode: "k3sctl-managed", + adapterServiceId: "k3sctl-adapter", + k3sServiceId: "code-queue", }, runtime: { - orchestrator: "v3sctl", + orchestrator: "k3sctl", providerStatus: "loading", providerName: "D601", }, @@ -1649,7 +1649,7 @@ function MicroserviceCatalogPage({ microservices, onRaw, onNavigate }: AnyRecord service.id === "claudeqq" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "claudeqq"), "data-testid": "open-claudeqq-button" }, "打开") : null, service.id === "baidu-netdisk" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "baidu-netdisk"), "data-testid": "open-baidu-netdisk-button" }, "打开") : null, service.id === "oa-event-flow" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "oa-event-flow"), "data-testid": "open-oa-event-flow-button" }, "打开") : null, - service.id === "v3sctl-adapter" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "v3sctl"), "data-testid": "open-v3sctl-button" }, "打开") : null, + service.id === "k3sctl-adapter" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "k3sctl"), "data-testid": "open-k3sctl-button" }, "打开") : null, service.id === "code-queue" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "open-code-queue-button" }, "打开") : null, service.id === "mdtodo" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "mdtodo"), "data-testid": "open-mdtodo-button" }, "打开") : null, service.id === "project-manager" ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "project-manager"), "data-testid": "open-project-manager-button" }, "打开") : null, @@ -2157,7 +2157,7 @@ function WorkArea({ activeModule, activeTab, data, session, refresh, onRaw, onNa if (activeModule === "apps" && activeTab === "baidu-netdisk") return h(BaiduNetdiskPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "apps" && activeTab === "filebrowser") return h(FileBrowserPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "apps" && activeTab === "oa-event-flow") return h(OaEventFlowPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); - if (activeModule === "apps" && activeTab === "v3sctl") return h(V3sCtlPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, onNavigate }); + if (activeModule === "apps" && activeTab === "k3sctl") return h(K3sCtlPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, onNavigate }); if (activeModule === "apps" && activeTab === "code-queue") return h(CodeQueuePage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl, initialTasksData: initialCodeQueueOverview }); if (activeModule === "apps" && activeTab === "mdtodo") return h(MdtodoPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); if (activeModule === "apps" && activeTab === "project-manager") return h(ProjectManagerPage, { microservices: data.microservices, onRaw, apiBaseUrl: cfg.apiBaseUrl }); @@ -2188,7 +2188,7 @@ function Shell({ session, onLogout }: AnyRecord) { : microservices; const effectiveData = effectiveMicroservices === microservices ? data : { ...data, microservices: effectiveMicroservices }; const activeService = activeModule === "apps" - ? effectiveMicroservices.find((service: any) => String(service?.id || "") === (activeTab === "v3sctl" ? "v3sctl-adapter" : activeTab)) + ? effectiveMicroservices.find((service: any) => String(service?.id || "") === (activeTab === "k3sctl" ? "k3sctl-adapter" : activeTab)) : null; const activeServiceRuntime = activeService ? microserviceRuntime(activeService) : {}; const activeTabTitle = module.tabs.find((tab: any) => tab.id === activeTab)?.label || activeTab; diff --git a/src/components/frontend/src/code-queue-entry.tsx b/src/components/frontend/src/code-queue-entry.tsx index da963ddf..fe43e81f 100644 --- a/src/components/frontend/src/code-queue-entry.tsx +++ b/src/components/frontend/src/code-queue-entry.tsx @@ -30,22 +30,22 @@ const standaloneCodeQueueService = { id: "code-queue", name: "Code Queue", providerId: "D601", - description: "Code Queue 独立入口,使用 v3sctl-adapter 单一路径访问 active service。", - repository: { containerName: "v3s:code-queue" }, + description: "Code Queue 独立入口,使用 k3sctl-adapter 单一路径访问 active service。", + repository: { containerName: "k3s:code-queue" }, backend: { - nodeBaseUrl: "v3s://code-queue", - nodeBindHost: "v3s://unidesk/code-queue", + nodeBaseUrl: "k3s://code-queue", + nodeBindHost: "k3s://unidesk/code-queue", nodePort: 4222, - proxyMode: "v3sctl-adapter-http", + proxyMode: "k3sctl-adapter-http", public: false, }, deployment: { - mode: "v3sctl-managed", - adapterServiceId: "v3sctl-adapter", - v3sServiceId: "code-queue", + mode: "k3sctl-managed", + adapterServiceId: "k3sctl-adapter", + k3sServiceId: "code-queue", }, runtime: { - orchestrator: "v3sctl", + orchestrator: "k3sctl", providerStatus: "online", providerName: "D601", }, diff --git a/src/components/frontend/src/v3sctl.tsx b/src/components/frontend/src/k3sctl.tsx similarity index 80% rename from src/components/frontend/src/v3sctl.tsx rename to src/components/frontend/src/k3sctl.tsx index 4281d296..b30f441f 100644 --- a/src/components/frontend/src/v3sctl.tsx +++ b/src/components/frontend/src/k3sctl.tsx @@ -57,11 +57,11 @@ function microserviceRuntime(service: any): AnyRecord { } function proxy(apiBaseUrl: string, path: string): string { - return `${apiBaseUrl}/microservices/v3sctl-adapter/proxy${path}`; + return `${apiBaseUrl}/microservices/k3sctl-adapter/proxy${path}`; } function findAdapter(microservices: any[]): any | null { - return microservices.find((service) => String(service?.id || "") === "v3sctl-adapter") || null; + return microservices.find((service) => String(service?.id || "") === "k3sctl-adapter") || null; } function instanceTone(instance: any): string { @@ -90,16 +90,16 @@ function primaryInstance(services: any[]): string { } function renderInstanceCard(instance: any) { - return h("article", { key: instance?.id || instance?.nodeId, className: "v3s-instance-card" }, + return h("article", { key: instance?.id || instance?.nodeId, className: "k3s-instance-card" }, h("div", { className: "node-card-head" }, h("strong", null, instance?.nodeId || instance?.id || "--"), h(StatusBadge, { status: instanceTone(instance) }, instance?.healthy ? "HEALTHY" : "DEGRADED"), ), - h("div", { className: "v3s-instance-role" }, + h("div", { className: "k3s-instance-role" }, h("span", null, String(instance?.role || "worker").toUpperCase()), h("code", null, instance?.id || "--"), ), - h("dl", { className: "v3s-kv" }, + h("dl", { className: "k3s-kv" }, h("dt", null, "Base URL"), h("dd", null, h("code", null, instance?.baseUrl || "--")), h("dt", null, "Proxy"), h("dd", null, instance?.proxyMode || "--"), h("dt", null, "Health"), h("dd", null, `${instance?.upstreamStatus ?? "--"} / ${instance?.status || "unknown"}`), @@ -114,23 +114,23 @@ function renderManagedService(service: any, onRaw: any) { return h(Panel, { key: service?.id || "service", title: service?.id || "managed-service", - eyebrow: `${service?.namespace || "unidesk"} / v3s managed service`, - className: "v3s-service-panel", - actions: h(RawButton, { title: `v3s service ${service?.id || ""}`, data: service, onOpen: onRaw, testId: `raw-v3s-service-${service?.id || "unknown"}` }), + eyebrow: `${service?.namespace || "unidesk"} / k3s managed service`, + className: "k3s-service-panel", + actions: h(RawButton, { title: `k3s service ${service?.id || ""}`, data: service, onOpen: onRaw, testId: `raw-k3s-service-${service?.id || "unknown"}` }), }, - h("div", { className: "v3s-service-summary" }, + h("div", { className: "k3s-service-summary" }, h("div", null, h("span", null, "状态"), h(StatusBadge, { status: serviceTone(service) }, service?.status || "unknown")), h("div", null, h("span", null, "Active"), h("strong", null, service?.activeInstanceId || "--")), h("div", null, h("span", null, "Single Writer"), h("strong", null, fmtBool(service?.singleWriter))), h("div", null, h("span", null, "Active Health"), h("strong", null, active?.upstreamStatus ?? "--")), ), instances.length === 0 - ? h(EmptyState, { title: "暂无 v3s 实例", text: "adapter 没有返回该服务的 endpoint 列表" }) - : h("div", { className: "v3s-instance-grid" }, instances.map(renderInstanceCard)), + ? h(EmptyState, { title: "暂无 k3s 实例", text: "adapter 没有返回该服务的 endpoint 列表" }) + : h("div", { className: "k3s-instance-grid" }, instances.map(renderInstanceCard)), ); } -export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: AnyRecord) { +export function K3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: AnyRecord) { const adapter = findAdapter(Array.isArray(microservices) ? microservices : []); const runtime = microserviceRuntime(adapter); const [state, setState] = useState({ loading: false, error: "", data: null as any, refreshedAt: null as Date | null }); @@ -141,7 +141,7 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any const data = await requestJson(proxy(apiBaseUrl, "/api/control-plane")); setState({ loading: false, error: "", data, refreshedAt: new Date() }); } catch (err) { - setState((previous: any) => ({ ...previous, loading: false, error: errorMessage(err, "加载 v3s 控制平面失败") })); + setState((previous: any) => ({ ...previous, loading: false, error: errorMessage(err, "加载 k3s 控制平面失败") })); } } @@ -157,50 +157,50 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any const kubeProxy = objectRecord(state.data?.kubeApiProxy); const manifestPaths = asArray(state.data?.manifestPaths).map((item) => String(item)); - if (!adapter) return h(EmptyState, { title: "v3sctl-adapter 未登记", text: "请在 config.json 的 microservices 中登记 id=v3sctl-adapter,并通过该微服务连接 v3s 控制平面。" }); + if (!adapter) return h(EmptyState, { title: "k3sctl-adapter 未登记", text: "请在 config.json 的 microservices 中登记 id=k3sctl-adapter,并通过该微服务连接 k3s 控制平面。" }); - return h("div", { className: "v3s-page", "data-testid": "v3sctl-page" }, + return h("div", { className: "k3s-page", "data-testid": "k3sctl-page" }, h(Panel, { - title: "V3S Control Plane", - eyebrow: "Managed by v3sctl-adapter", - className: "v3s-hero-panel", + title: "K3S Control Plane", + eyebrow: "Managed by k3sctl-adapter", + className: "k3s-hero-panel", loading: state.loading, actions: h(React.Fragment, null, - h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "v3s-refresh-button" }, state.loading ? "刷新中" : "刷新"), - onNavigate ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "v3s-open-code-queue" }, "打开 Code Queue") : null, - h(RawButton, { title: "v3sctl-adapter microservice", data: adapter, onOpen: onRaw, testId: "raw-v3s-adapter" }), + h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "k3s-refresh-button" }, state.loading ? "刷新中" : "刷新"), + onNavigate ? h("button", { type: "button", className: "ghost-btn", onClick: () => onNavigate("apps", "code-queue"), "data-testid": "k3s-open-code-queue" }, "打开 Code Queue") : null, + h(RawButton, { title: "k3sctl-adapter microservice", data: adapter, onOpen: onRaw, testId: "raw-k3s-adapter" }), ), }, - h("div", { className: "v3s-hero" }, - h("div", { className: "v3s-orb", "aria-hidden": "true" }, h("span", null, "v3s")), - h("div", { className: "v3s-hero-copy" }, + h("div", { className: "k3s-hero" }, + h("div", { className: "k3s-orb", "aria-hidden": "true" }, h("span", null, "k3s")), + h("div", { className: "k3s-hero-copy" }, h("p", { className: "eyebrow" }, "D601 control plane / D518 managed node"), - h("h2", null, "UniDesk 只管理 adapter;业务微服务交给 v3s 标准服务路由"), - h("p", { className: "muted paragraph" }, "Code Queue 的前端/API 请求进入 v3sctl-adapter,再由 adapter 转发到 v3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"), - h("div", { className: "v3s-route-strip" }, + h("h2", null, "UniDesk 只管理 adapter;业务微服务交给 k3s 标准服务路由"), + h("p", { className: "muted paragraph" }, "Code Queue 的前端/API 请求进入 k3sctl-adapter,再由 adapter 转发到 k3s active service。provider-gateway 只用于维护 adapter 和节点诊断,不再直接管理 Code Queue 容器。"), + h("div", { className: "k3s-route-strip" }, h("span", null, "NO FALLBACK"), - h("code", null, state.data?.runtimePath || "frontend -> backend-core -> v3sctl-adapter"), + h("code", null, state.data?.runtimePath || "frontend -> backend-core -> k3sctl-adapter"), ), ), ), h("div", { className: "metric-grid" }, h(MetricCard, { label: "控制面", value: state.data?.clusterId || "D601", hint: `adapter ${runtime.providerStatus || "unknown"}`, tone: runtime.providerStatus === "online" ? "ok" : "warn" }), h(MetricCard, { label: "代管服务", value: services.length, hint: `${healthyServices}/${services.length || 0} healthy`, tone: healthyServices === services.length && services.length > 0 ? "ok" : "warn" }), - h(MetricCard, { label: "节点", value: nodes.join(" / ") || "--", hint: "expected v3s nodes" }), + h(MetricCard, { label: "节点", value: nodes.join(" / ") || "--", hint: "expected k3s nodes" }), h(MetricCard, { label: "实例", value: `${healthyInstances}/${totalInstances}`, hint: `active ${primary}`, tone: healthyInstances === totalInstances && totalInstances > 0 ? "ok" : "warn" }), ), - h("div", { className: "v3s-control-plane-grid" }, - h("article", { className: "v3s-control-plane-card" }, + h("div", { className: "k3s-control-plane-grid" }, + h("article", { className: "k3s-control-plane-card" }, h("span", null, "service proxy"), h("strong", null, kubeProxy.configured === true ? "K8S API PROXY" : "PROXY DEGRADED"), h("p", null, kubeProxy.configured === true ? `${kubeProxy.mode || "kubernetes-api-service-proxy"} via ${kubeProxy.connectHost || "--"}` : "adapter 必须通过 k8s API service proxy 访问业务服务,不回退到业务容器直连。"), ), - h("article", { className: "v3s-control-plane-card" }, + h("article", { className: "k3s-control-plane-card" }, h("span", null, "manifests"), h("strong", null, manifestPaths.length || "--"), h("p", null, manifestPaths.join(" / ") || "未配置 manifest"), ), - h("article", { className: "v3s-control-plane-card" }, + h("article", { className: "k3s-control-plane-card" }, h("span", null, "cluster snapshot"), h("strong", null, kubectl.enabled === true ? (kubectl.ok === true ? "KUBECTL OK" : "KUBECTL DEGRADED") : "API ONLY"), h("p", null, kubectl.enabled === true ? `nodes ${kubectl.nodeCount ?? "--"}` : "控制面页面以 adapter 返回的 k8s service proxy 状态为准;kubectl 只作为可选快照。"), @@ -210,7 +210,7 @@ export function V3sCtlPage({ microservices, onRaw, apiBaseUrl, onNavigate }: Any state.refreshedAt ? h("p", { className: "muted paragraph" }, `最近刷新 ${fmtClock(state.refreshedAt)}`) : null, ), services.length === 0 - ? h(Panel, { title: "代管服务", eyebrow: "v3s services", loading: state.loading }, h(EmptyState, { title: "暂无 v3s 服务", text: "等待 v3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。" })) + ? h(Panel, { title: "代管服务", eyebrow: "k3s services", loading: state.loading }, h(EmptyState, { title: "暂无 k3s 服务", text: "等待 k3sctl-adapter 返回 /api/services;Code Queue 切换后这里应显示 D601 和 D518 两个实例。" })) : services.map((service) => renderManagedService(service, onRaw)), ); } diff --git a/src/components/frontend/src/navigation.ts b/src/components/frontend/src/navigation.ts index d6dd15de..f40af859 100644 --- a/src/components/frontend/src/navigation.ts +++ b/src/components/frontend/src/navigation.ts @@ -70,7 +70,7 @@ export const MODULES: UniDeskModuleDefinition[] = [ { id: "baidu-netdisk", label: "Baidu Netdisk" }, { id: "filebrowser", label: "File Browser" }, { id: "oa-event-flow", label: "OA Event Flow" }, - { id: "v3sctl", label: "V3S Control" }, + { id: "k3sctl", label: "K3S Control" }, { id: "code-queue", label: "Code Queue" }, { id: "mdtodo", label: "MDTODO" }, { id: "project-manager", label: "Project Manager" }, diff --git a/src/components/microservices/code-queue/src/index.ts b/src/components/microservices/code-queue/src/index.ts index fa46c44c..d5246889 100644 --- a/src/components/microservices/code-queue/src/index.ts +++ b/src/components/microservices/code-queue/src/index.ts @@ -3109,7 +3109,7 @@ function queuedStatusReason(task: QueueTask, tasks: QueueTask[] = state.tasks): return queuedReason("service", "SERVICE", "Code Queue is still starting and has not enabled scheduling yet."); } if (!config.schedulerEnabled) { - return queuedReason("scheduler_disabled", "STANDBY", "This Code Queue instance is a v3s-managed standby and does not start queued work."); + return queuedReason("scheduler_disabled", "STANDBY", "This Code Queue instance is a k3s-managed standby and does not start queued work."); } if (mergingQueues.has(queueId)) { return queuedReason("queue_merge", "MERGING", "Queue merge is rewriting queue membership; scheduling will resume immediately after it finishes."); diff --git a/src/components/microservices/v3sctl-adapter/Dockerfile b/src/components/microservices/k3sctl-adapter/Dockerfile similarity index 62% rename from src/components/microservices/v3sctl-adapter/Dockerfile rename to src/components/microservices/k3sctl-adapter/Dockerfile index 6ff59a68..a49edc99 100644 --- a/src/components/microservices/v3sctl-adapter/Dockerfile +++ b/src/components/microservices/k3sctl-adapter/Dockerfile @@ -1,5 +1,5 @@ -ARG V3SCTL_ADAPTER_BASE_IMAGE=oven/bun:1-debian -FROM ${V3SCTL_ADAPTER_BASE_IMAGE} +ARG K3SCTL_ADAPTER_BASE_IMAGE=oven/bun:1-debian +FROM ${K3SCTL_ADAPTER_BASE_IMAGE} # Never build the adapter FROM a service image: inherited Docker Desktop labels # can silently republish old Code Queue ports and mounts. @@ -12,12 +12,12 @@ RUN (command -v curl >/dev/null 2>&1 && command -v ssh >/dev/null 2>&1 && comman && apt-get clean \ && rm -rf /var/lib/apt/lists/*) -WORKDIR /app/src/components/microservices/v3sctl-adapter +WORKDIR /app/src/components/microservices/k3sctl-adapter COPY src/components/shared /app/src/components/shared -COPY src/components/microservices/v3sctl-adapter/package.json ./package.json -COPY src/components/microservices/v3sctl-adapter/tsconfig.json ./tsconfig.json -COPY src/components/microservices/v3sctl-adapter/src ./src -COPY src/components/microservices/v3sctl-adapter/v3s ./v3s +COPY src/components/microservices/k3sctl-adapter/package.json ./package.json +COPY src/components/microservices/k3sctl-adapter/tsconfig.json ./tsconfig.json +COPY src/components/microservices/k3sctl-adapter/src ./src +COPY src/components/microservices/k3sctl-adapter/k3s ./k3s EXPOSE 4266 CMD ["bun", "--smol", "run", "src/index.ts"] diff --git a/src/components/microservices/k3sctl-adapter/docker-compose.d601.yml b/src/components/microservices/k3sctl-adapter/docker-compose.d601.yml new file mode 100644 index 00000000..f30cbef0 --- /dev/null +++ b/src/components/microservices/k3sctl-adapter/docker-compose.d601.yml @@ -0,0 +1,46 @@ +services: + k3sctl-adapter: + image: unidesk-k3sctl-adapter:d601 + build: + context: ../../../.. + dockerfile: src/components/microservices/k3sctl-adapter/Dockerfile + args: + K3SCTL_ADAPTER_BASE_IMAGE: ${K3SCTL_ADAPTER_BASE_IMAGE:-oven/bun:1-debian} + container_name: k3sctl-adapter + restart: unless-stopped + env_file: + - path: ${K3SCTL_ADAPTER_ENV_FILE:-../../../../.state/k3sctl-adapter-d601.env} + required: false + ports: + - "127.0.0.1:${K3SCTL_ADAPTER_HOST_PORT:-4266}:4266" + environment: + HOST: "0.0.0.0" + PORT: "4266" + LOG_FILE: "/var/log/unidesk/k3sctl-adapter.jsonl" + K3SCTL_CLUSTER_ID: "${K3SCTL_CLUSTER_ID:-unidesk-k3s}" + K3SCTL_NODE_ID: "${K3SCTL_NODE_ID:-D601}" + K3SCTL_KUBECTL_ENABLED: "${K3SCTL_KUBECTL_ENABLED:-false}" + K3SCTL_KUBE_API_PROXY_ENABLED: "${K3SCTL_KUBE_API_PROXY_ENABLED:-true}" + K3SCTL_KUBECONFIG_PATH: "/var/lib/unidesk/k3s/kubeconfig" + K3SCTL_KUBE_API_CONNECT_HOST: "${K3SCTL_KUBE_API_CONNECT_HOST:-host.docker.internal}" + K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json}" + K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}" + UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}" + volumes: + - ${K3SCTL_ADAPTER_LOG_DIR:-../../../../.state/k3sctl-adapter/logs}:/var/log/unidesk + - ${K3SCTL_KUBECONFIG_HOST_PATH:-/etc/rancher/k3s/k3s.yaml}:/var/lib/unidesk/k3s/kubeconfig:ro + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - default + - provider-gateway + healthcheck: + test: ["CMD-SHELL", "curl -fsS --max-time 2 http://127.0.0.1:4266/health >/dev/null"] + interval: 5s + timeout: 3s + retries: 20 + +networks: + provider-gateway: + external: true + name: ${K3SCTL_PROVIDER_GATEWAY_NETWORK:-unidesk-provider-d601_default} diff --git a/src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json similarity index 87% rename from src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json rename to src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json index 7bf7576d..43717629 100644 --- a/src/components/microservices/v3sctl-adapter/v3s/code-queue.v3s.json +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json @@ -1,16 +1,16 @@ { - "apiVersion": "unidesk.ai/v3s/v1", + "apiVersion": "unidesk.ai/k3s/v1", "kind": "ManagedKubernetesService", "metadata": { "name": "code-queue", "namespace": "unidesk" }, "spec": { - "adapterServiceId": "v3sctl-adapter", + "adapterServiceId": "k3sctl-adapter", "controlPlane": { "type": "kubernetes", - "cluster": "unidesk-v8s", - "context": "unidesk-v8s" + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" }, "route": { "kind": "kubernetes-service", diff --git a/src/components/microservices/v3sctl-adapter/v3s/code-queue.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml similarity index 98% rename from src/components/microservices/v3sctl-adapter/v3s/code-queue.k8s.yaml rename to src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml index b65789df..e37e4466 100644 --- a/src/components/microservices/v3sctl-adapter/v3s/code-queue.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml @@ -4,7 +4,7 @@ metadata: name: unidesk labels: app.kubernetes.io/part-of: unidesk - unidesk.ai/v3s-cluster: unidesk-v8s + unidesk.ai/k3s-cluster: unidesk-k3s --- apiVersion: v1 kind: Service @@ -255,7 +255,7 @@ metadata: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D601 spec: replicas: 1 @@ -268,7 +268,7 @@ spec: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D601 unidesk.ai/node-id: D601 spec: @@ -480,7 +480,7 @@ metadata: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed spec: type: ClusterIP selector: @@ -500,7 +500,7 @@ metadata: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D518 spec: replicas: 1 @@ -513,7 +513,7 @@ spec: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D518 unidesk.ai/node-id: D518 spec: @@ -725,7 +725,7 @@ metadata: labels: app.kubernetes.io/name: code-queue app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D518 spec: type: ClusterIP diff --git a/src/components/microservices/v3sctl-adapter/v3s/mdtodo.v3s.json b/src/components/microservices/k3sctl-adapter/k3s/mdtodo.k3s.json similarity index 83% rename from src/components/microservices/v3sctl-adapter/v3s/mdtodo.v3s.json rename to src/components/microservices/k3sctl-adapter/k3s/mdtodo.k3s.json index 4377133f..9c936466 100644 --- a/src/components/microservices/v3sctl-adapter/v3s/mdtodo.v3s.json +++ b/src/components/microservices/k3sctl-adapter/k3s/mdtodo.k3s.json @@ -1,16 +1,16 @@ { - "apiVersion": "unidesk.ai/v3s/v1", + "apiVersion": "unidesk.ai/k3s/v1", "kind": "ManagedKubernetesService", "metadata": { "name": "mdtodo", "namespace": "unidesk" }, "spec": { - "adapterServiceId": "v3sctl-adapter", + "adapterServiceId": "k3sctl-adapter", "controlPlane": { "type": "kubernetes", - "cluster": "unidesk-v8s", - "context": "unidesk-v8s" + "cluster": "unidesk-k3s", + "context": "unidesk-k3s" }, "route": { "kind": "kubernetes-service", diff --git a/src/components/microservices/v3sctl-adapter/v3s/mdtodo.k8s.yaml b/src/components/microservices/k3sctl-adapter/k3s/mdtodo.k8s.yaml similarity index 94% rename from src/components/microservices/v3sctl-adapter/v3s/mdtodo.k8s.yaml rename to src/components/microservices/k3sctl-adapter/k3s/mdtodo.k8s.yaml index b96287d2..2574c05e 100644 --- a/src/components/microservices/v3sctl-adapter/v3s/mdtodo.k8s.yaml +++ b/src/components/microservices/k3sctl-adapter/k3s/mdtodo.k8s.yaml @@ -6,7 +6,7 @@ metadata: labels: app.kubernetes.io/name: mdtodo app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D601 spec: replicas: 1 @@ -19,7 +19,7 @@ spec: labels: app.kubernetes.io/name: mdtodo app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed unidesk.ai/instance-id: D601 unidesk.ai/node-id: D601 spec: @@ -94,7 +94,7 @@ metadata: labels: app.kubernetes.io/name: mdtodo app.kubernetes.io/part-of: unidesk - unidesk.ai/deployment-mode: v3sctl-managed + unidesk.ai/deployment-mode: k3sctl-managed spec: type: ClusterIP selector: diff --git a/src/components/microservices/v3sctl-adapter/package.json b/src/components/microservices/k3sctl-adapter/package.json similarity index 79% rename from src/components/microservices/v3sctl-adapter/package.json rename to src/components/microservices/k3sctl-adapter/package.json index 769ae508..32164e17 100644 --- a/src/components/microservices/v3sctl-adapter/package.json +++ b/src/components/microservices/k3sctl-adapter/package.json @@ -1,5 +1,5 @@ { - "name": "@unidesk/v3sctl-adapter", + "name": "@unidesk/k3sctl-adapter", "private": true, "type": "module", "scripts": { diff --git a/src/components/microservices/v3sctl-adapter/src/index.ts b/src/components/microservices/k3sctl-adapter/src/index.ts similarity index 94% rename from src/components/microservices/v3sctl-adapter/src/index.ts rename to src/components/microservices/k3sctl-adapter/src/index.ts index ef7d8a24..cdc0ee92 100644 --- a/src/components/microservices/v3sctl-adapter/src/index.ts +++ b/src/components/microservices/k3sctl-adapter/src/index.ts @@ -64,8 +64,8 @@ const config = readConfig(); const logWriter = config.logFile ? createHourlyJsonlWriter({ baseLogFile: config.logFile, - service: "v3sctl-adapter", - maxBytes: logRetentionBytesForService("v3sctl-adapter"), + service: "k3sctl-adapter", + maxBytes: logRetentionBytesForService("k3sctl-adapter"), }) : null; const kubeClient = loadKubeApiClient(); @@ -172,7 +172,7 @@ function parseManagedKubernetesManifest(value: unknown, index: number, ownerPath const spec = asRecord(manifest.spec, `${path}.spec`); const kind = optionalStringField(manifest, "kind", "ManagedKubernetesService"); const serviceId = stringField(metadata, "name", `${path}.metadata`); - if (kind !== "ManagedKubernetesService") throw new Error(`${path}.kind must be ManagedKubernetesService; direct ManagedHttpService manifests are not allowed in pure v3s mode`); + if (kind !== "ManagedKubernetesService") throw new Error(`${path}.kind must be ManagedKubernetesService; direct ManagedHttpService manifests are not allowed in pure k3s mode`); const instancesRaw = spec.instances; if (!Array.isArray(instancesRaw) || instancesRaw.length === 0) throw new Error(`${path}.spec.instances must be a non-empty array`); const endpoints = instancesRaw.map((endpoint, endpointIndex) => parseEndpoint(endpoint, endpointIndex, `${path}.spec.instances`)); @@ -200,12 +200,12 @@ function parseServiceOrManifest(value: unknown, index: number, ownerPath = "serv if (typeof item.kind === "string" || item.metadata !== undefined || item.spec !== undefined) { return parseManagedKubernetesManifest(item, index, ownerPath); } - throw new Error(`${ownerPath}[${index}] must be a ManagedKubernetesService manifest; static HTTP service declarations are not allowed in pure v3s mode`); + throw new Error(`${ownerPath}[${index}] must be a ManagedKubernetesService manifest; static HTTP service declarations are not allowed in pure k3s mode`); } -function parseServices(raw: string, ownerPath = "V3SCTL_SERVICES_JSON"): ManagedService[] { +function parseServices(raw: string, ownerPath = "K3SCTL_SERVICES_JSON"): ManagedService[] { const value = raw.trim().length === 0 ? [] : JSON.parse(raw) as unknown; - if (!Array.isArray(value)) throw new Error("V3SCTL_SERVICES_JSON must be an array"); + if (!Array.isArray(value)) throw new Error("K3SCTL_SERVICES_JSON must be an array"); return value.map((item, index) => parseServiceOrManifest(item, index, ownerPath)); } @@ -237,36 +237,36 @@ function mergeServices(services: ManagedService[]): ManagedService[] { const seen = new Set(); for (const service of services) { const key = `${service.namespace}/${service.id}`; - if (seen.has(key)) throw new Error(`duplicate v3s managed service: ${key}`); + if (seen.has(key)) throw new Error(`duplicate k3s managed service: ${key}`); seen.add(key); } return services; } function readConfig(): RuntimeConfig { - const paths = manifestPaths(envString("V3SCTL_MANIFEST_PATHS", "v3s/code-queue.v3s.json")); - const inlineServices = parseServices(envString("V3SCTL_SERVICES_JSON", "[]")); + const paths = manifestPaths(envString("K3SCTL_MANIFEST_PATHS", "k3s/code-queue.k3s.json")); + const inlineServices = parseServices(envString("K3SCTL_SERVICES_JSON", "[]")); const manifestServices = readManifestServices(paths); return { host: envString("HOST", "0.0.0.0"), port: envNumber("PORT", 4266), - logFile: envString("LOG_FILE", "/var/log/unidesk/v3sctl-adapter.jsonl"), + logFile: envString("LOG_FILE", "/var/log/unidesk/k3sctl-adapter.jsonl"), manifestPaths: paths, - clusterId: envString("V3SCTL_CLUSTER_ID", "unidesk-v8s"), - nodeId: envString("V3SCTL_NODE_ID", "D601"), - kubectlEnabled: envBool("V3SCTL_KUBECTL_ENABLED", false), - kubectlContext: envString("V3SCTL_KUBECTL_CONTEXT", ""), - kubeApiProxyEnabled: envBool("V3SCTL_KUBE_API_PROXY_ENABLED", true), - kubeconfigPath: envString("V3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/v8s/kubeconfig"), - kubeApiConnectHost: envString("V3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"), - requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("V3SCTL_REQUEST_TIMEOUT_MS", 30_000))), - healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("V3SCTL_HEALTH_TIMEOUT_MS", 2500))), + clusterId: envString("K3SCTL_CLUSTER_ID", "unidesk-k3s"), + nodeId: envString("K3SCTL_NODE_ID", "D601"), + kubectlEnabled: envBool("K3SCTL_KUBECTL_ENABLED", false), + kubectlContext: envString("K3SCTL_KUBECTL_CONTEXT", ""), + kubeApiProxyEnabled: envBool("K3SCTL_KUBE_API_PROXY_ENABLED", true), + kubeconfigPath: envString("K3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/k3s/kubeconfig"), + kubeApiConnectHost: envString("K3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"), + requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("K3SCTL_REQUEST_TIMEOUT_MS", 30_000))), + healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("K3SCTL_HEALTH_TIMEOUT_MS", 2500))), services: mergeServices([...manifestServices, ...inlineServices]), }; } function log(level: "debug" | "info" | "warn" | "error", event: string, detail: JsonRecord = {}): void { - const record: JsonRecord = { at: new Date().toISOString(), service: "v3sctl-adapter", level, event, ...detail }; + const record: JsonRecord = { at: new Date().toISOString(), service: "k3sctl-adapter", level, event, ...detail }; recentLogs.push(record); while (recentLogs.length > 500) recentLogs.shift(); try { @@ -304,7 +304,7 @@ function loadKubeApiClient(): KubeApiClient | null { const cert = kubeconfigScalar(raw, "client-certificate-data"); const key = kubeconfigScalar(raw, "client-key-data"); if (server.length === 0 || ca.length === 0 || cert.length === 0 || key.length === 0) throw new Error("kubeconfig must include server, CA, client certificate, and client key data"); - const dir = join(tmpdir(), `unidesk-v3sctl-kube-${config.clusterId}`); + const dir = join(tmpdir(), `unidesk-k3sctl-kube-${config.clusterId}`); mkdirSync(dir, { recursive: true, mode: 0o700 }); const client: KubeApiClient = { serverUrl: new URL(server), @@ -510,7 +510,7 @@ async function kubeApiProxyResponse( headers: { "content-type": parsed.contentType, "x-unidesk-proxy-mode": "kubernetes-api-service-proxy", - "x-unidesk-v3s-service": service.id, + "x-unidesk-k3s-service": service.id, "x-unidesk-response-truncated": "false", }, }); @@ -577,7 +577,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage const response = active ? await kubeApiServiceProxyResponse( service, - new Request("http://v3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }), + new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }), endpoint.healthPath, "", config.healthTimeoutMs, @@ -585,7 +585,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage : await kubeApiEndpointProxyResponse( service, endpoint, - new Request("http://v3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }), + new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }), endpoint.healthPath, "", config.healthTimeoutMs, @@ -650,7 +650,7 @@ async function probeKubernetesPodReady(service: ManagedService, endpoint: Manage }).toString(); const response = await kubeApiProxyResponse( service, - new Request("http://v3sctl-adapter.local/api/pods", { method: "GET", headers: { accept: "application/json" } }), + new Request("http://k3sctl-adapter.local/api/pods", { method: "GET", headers: { accept: "application/json" } }), `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods`, `?${labelSelector}`, config.healthTimeoutMs, @@ -700,7 +700,7 @@ async function serviceStatus(service: ManagedService): Promise { upstreamStatus: null, contentType: null, checkedAt: new Date().toISOString(), - error: "v3s managed service route must be kubernetes-service", + error: "k3s managed service route must be kubernetes-service", noFallback: true, }]; const active = instances.find((item) => item.id === service.activeInstanceId) ?? null; @@ -754,14 +754,14 @@ async function controlPlaneSnapshot(): Promise { const managedServicesHealthy = services.every((service) => service.healthy === true); return { ok: true, - service: "v3sctl-adapter", + service: "k3sctl-adapter", clusterId: config.clusterId, nodeId: config.nodeId, startedAt, manifestPaths: config.manifestPaths, managedServicesHealthy, noFallback: true, - runtimePath: "frontend -> backend-core -> v3sctl-adapter -> kubernetes api service proxy -> v3s service", + runtimePath: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service", kubeApiProxy: { enabled: config.kubeApiProxyEnabled, configured: kubeClient !== null, @@ -788,8 +788,8 @@ async function proxyToService(service: ManagedService, req: Request, targetPath: if (isKubernetesServiceRoute(service)) { return kubeApiServiceProxyResponse(service, req, targetPath, query, config.requestTimeoutMs); } - log("error", "v3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true }); - return jsonResponse({ ok: false, error: "v3s managed service route must be kubernetes-service", serviceId: service.id, route: service.route, noFallback: true }, 500); + log("error", "k3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true }); + return jsonResponse({ ok: false, error: "k3s managed service route must be kubernetes-service", serviceId: service.id, route: service.route, noFallback: true }, 500); } async function route(req: Request): Promise { @@ -800,7 +800,7 @@ async function route(req: Request): Promise { const snapshot = await controlPlaneSnapshot(); return jsonResponse({ ok: true, - service: "v3sctl-adapter", + service: "k3sctl-adapter", clusterId: config.clusterId, nodeId: config.nodeId, startedAt, diff --git a/src/components/microservices/v3sctl-adapter/tsconfig.json b/src/components/microservices/k3sctl-adapter/tsconfig.json similarity index 100% rename from src/components/microservices/v3sctl-adapter/tsconfig.json rename to src/components/microservices/k3sctl-adapter/tsconfig.json diff --git a/src/components/microservices/v3sctl-adapter/docker-compose.d601.yml b/src/components/microservices/v3sctl-adapter/docker-compose.d601.yml deleted file mode 100644 index 68070208..00000000 --- a/src/components/microservices/v3sctl-adapter/docker-compose.d601.yml +++ /dev/null @@ -1,46 +0,0 @@ -services: - v3sctl-adapter: - image: unidesk-v3sctl-adapter:d601 - build: - context: ../../../.. - dockerfile: src/components/microservices/v3sctl-adapter/Dockerfile - args: - V3SCTL_ADAPTER_BASE_IMAGE: ${V3SCTL_ADAPTER_BASE_IMAGE:-oven/bun:1-debian} - container_name: v3sctl-adapter - restart: unless-stopped - env_file: - - path: ${V3SCTL_ADAPTER_ENV_FILE:-../../../../.state/v3sctl-adapter-d601.env} - required: false - ports: - - "127.0.0.1:${V3SCTL_ADAPTER_HOST_PORT:-4266}:4266" - environment: - HOST: "0.0.0.0" - PORT: "4266" - LOG_FILE: "/var/log/unidesk/v3sctl-adapter.jsonl" - V3SCTL_CLUSTER_ID: "${V3SCTL_CLUSTER_ID:-unidesk-v8s}" - V3SCTL_NODE_ID: "${V3SCTL_NODE_ID:-D601}" - V3SCTL_KUBECTL_ENABLED: "${V3SCTL_KUBECTL_ENABLED:-false}" - V3SCTL_KUBE_API_PROXY_ENABLED: "${V3SCTL_KUBE_API_PROXY_ENABLED:-true}" - V3SCTL_KUBECONFIG_PATH: "/var/lib/unidesk/v8s/kubeconfig" - V3SCTL_KUBE_API_CONNECT_HOST: "${V3SCTL_KUBE_API_CONNECT_HOST:-host.docker.internal}" - V3SCTL_MANIFEST_PATHS: "${V3SCTL_MANIFEST_PATHS:-v3s/code-queue.v3s.json,v3s/mdtodo.v3s.json}" - V3SCTL_SERVICES_JSON: "${V3SCTL_SERVICES_JSON:-[]}" - UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}" - volumes: - - ${V3SCTL_ADAPTER_LOG_DIR:-../../../../.state/v3sctl-adapter/logs}:/var/log/unidesk - - ${V3SCTL_KUBECONFIG_HOST_PATH:-../../../../.state/v8s/kubeconfig}:/var/lib/unidesk/v8s/kubeconfig:ro - extra_hosts: - - "host.docker.internal:host-gateway" - networks: - - default - - provider-gateway - healthcheck: - test: ["CMD-SHELL", "curl -fsS --max-time 2 http://127.0.0.1:4266/health >/dev/null"] - interval: 5s - timeout: 3s - retries: 20 - -networks: - provider-gateway: - external: true - name: ${V3SCTL_PROVIDER_GATEWAY_NETWORK:-unidesk-provider-d601_default} diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index c160f471..f0b8b3e0 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -6,7 +6,7 @@ { "path": "components/provider-gateway" }, { "path": "components/frontend" }, { "path": "components/microservices/code-queue" }, - { "path": "components/microservices/v3sctl-adapter" }, + { "path": "components/microservices/k3sctl-adapter" }, { "path": "components/microservices/mdtodo" }, { "path": "components/microservices/project-manager" }, { "path": "components/microservices/baidu-netdisk" }