42 Commits

Author SHA1 Message Date
Codex Release Bot 00c9eddd19 chore(mcp): add axs gitea mcp configuration
Nexus CI/CD / test (pull_request) Waiting to run
Nexus CI/CD / e2e (pull_request) Blocked by required conditions
Nexus CI/CD / deploy (pull_request) Blocked by required conditions
2026-07-09 06:25:39 +08:00
Codex Release Bot 6aaf292ad9 release: merge btpanel fixes app-v2 and gitea allowlist 2026-07-09 06:03:37 +08:00
r a16ba169fd fix(btpanel): 未装宝塔时一键登录返回友好提示
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
SSH/API 登录前检测面板;将 public 模块 Traceback 映射为可读中文;
bootstrap bt_not_installed 不再回退 SSH。AGENTS 约定改完推 Gitea。
2026-07-09 01:25:54 +08:00
r 9b583ccc91 docs(release): 主与 release 分支合并功能清单 + 导入 release 文档目录
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
便于 Gitea 上 main ↔ release/20260708-axs 合并评审与冲突对照。
2026-07-09 01:14:32 +08:00
r 5520434d53 build(frontend): 同步 web/app 构建产物与 ServersPage 调整
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
与 axs 全量重传前的本地工作区对齐。
2026-07-09 00:16:56 +08:00
r bd041136c0 chore(deploy): axs Gitea 为默认发布源 + 全量分批推送脚本
主项目推送到 https://axs.kuma1xn.vip 时分批提交避免 Cloudflare 524。
2026-07-08 23:58:52 +08:00
r 6859846b7c sync: 主项目全量同步(含 btpanel 一键登录回归修复与前端构建产物)
- bootstrap 失败恢复 SSH 临时登录回退
- ssh_bootstrap 真安装检测与 config 目录自愈
- axs Gitea 发布源切换为 https://axs.kuma1xn.vip
2026-07-08 23:52:40 +08:00
r d976cd03a3 feat(terminal): 工具栏增加宝塔一键登录按钮
终端有打开会话时,在字号左侧提供宝塔登录,复用 useBtPanelLogin。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 23:19:16 +08:00
Nexus Gate 41bff9dff6 补录 Gate 8 AI Review 缓存(65d8605b 批次) 2026-07-01 12:10:07 +08:00
Nexus Gate 65d8605b6c 补录 2026-07-01 部署门禁:Terminal 快速添加、快捷命令排序、Gate3 pytest 回退
6/30 生产部署跳过 pre_deploy_check;本提交补齐 changelog/audit 与门控修复。
2026-07-01 12:09:02 +08:00
r d50120d1d2 每日离线巡检发报前 SSH 复检离线子机并刷新心跳。
日报二次扫描反映复检后状态,Telegram 展示复检统计;离线列表与告警显示名优先备注。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 17:04:37 +08:00
r 0ec0aaf174 feat(btpanel): 一键登录临时 token 有效期改为 24 小时
API set_temp_login 传 expire_time=86400;SSH 回退用自定义脚本替代 tools.py 3 小时默认。
2026-06-22 05:48:15 +08:00
Nexus Agent 45d21026b7 feat(frontend): 跨服务器传输源机 A 展示可点击站点链接
与服务器列表同源 resolveServerSiteUrl;选 A 后显示域名链接。
2026-06-21 13:20:03 +08:00
Nexus Agent 71f0f53e6a feat(frontend): 跨服务器传输目标机 B 文件浏览
选 B 后展示目录列表,浏览时同步目标路径。
2026-06-21 13:15:01 +08:00
Nexus Agent 113718b553 fix(frontend): 跨服务器传输默认使用各机目标路径
选源机浏览 A.target_path;选目标机填入 B.target_path。
2026-06-21 13:09:48 +08:00
Nexus Agent da26d648a9 fix(frontend): 跨服务器传输页服务器选择与搜索
对齐文件页 v-autocomplete:v-model、客户端过滤、修正选项 slot。
2026-06-21 13:05:39 +08:00
Nexus Agent d770b07609 feat(frontend): 跨服务器传输独立页与侧栏菜单调整
将传输功能从文件页拆到 #/server-transfer,文件页改为跳转预填;监测历史移入系统分组。
2026-06-21 12:59:45 +08:00
r 73278667e0 feat(files): 跨服务器文件直传与打包投递
A→B SFTP 中继与一键 tar 打包 curl 拉取;含审计修复与 18 项单测。
2026-06-21 12:48:50 +08:00
r 76682e3be0 ui(nav): 侧栏隐藏宝塔面板菜单分组
宝塔能力保留在 servers 页一键登录与批量获取 API;btpanel 路由仍可通过直链访问。
2026-06-21 12:22:14 +08:00
r 629d8b3e26 fix(btpanel): login-url 返回类型含 bool 导致全站 500
路由注解 dict[str,str] 与 bootstrapped 布尔字段冲突,FastAPI 响应校验失败。
2026-06-21 12:17:29 +08:00
r 37720c4705 feat(btpanel): 一键登录自动获取 API 与倒计时提示
未配置机器点击登录时后端 immediate bootstrap;前端顶部 snackbar 显示约 20 秒倒计时。
2026-06-21 12:09:34 +08:00
r 4f260f9257 fix(servers): 批量栏已选区增加「选择」标题对齐
与运维/Agent/其他分组同结构底对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:41:30 +08:00
r c8efbc2a90 refactor(servers): 行操作移除删除按钮
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
删除改由批量栏「批量删除」执行,降低误删风险。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:35:47 +08:00
r 2cb0b57281 fix(servers): 提高行操作与批量栏文字可读性
tonal/flat 实底按钮、加大字号字重,恢复一键登录全文。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:32:32 +08:00
r 80d6aad54b style(servers): 行操作与批量栏分组图标化 UI
outlined 按钮组 + 批量栏运维/Agent/其他分区展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:29:27 +08:00
r 58843a1ef4 refactor(servers): 精简行操作并收拢批量诊断
一键登录文案、移除监测/站点行按钮,检测路径与诊断移至批量栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:23:39 +08:00
r 7add9384c1 feat: 终端路由保持会话 + 服务器列表按创建时间倒序
keep-alive 下终端切换不 dispose SSH;列表/推送/终端默认 newest first。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:11:00 +08:00
r 6720ca3474 feat(servers): 主列表宝塔登录 + 健康检查写心跳
主服务器列表操作列一键登录宝塔;SSH 健康检查写入 Redis 心跳并刷新在线状态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 11:05:43 +08:00
r 2e797f86b7 feat(btpanel): 服务器列表增加在线状态列
批量读 Redis 心跳返回 status,前端与主服务器列表样式一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:54:04 +08:00
r 5ea5c3a411 fix(btpanel): 一键登录 URL 去掉安全入口路径
tmp_token 登录在 :port/login,非 :port/{admin}/login,后者 nginx 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:46:22 +08:00
r 3195787461 fix(btpanel): 非 root SSH bootstrap 自动 sudo 提权
ubuntu 等用户免密 sudo 下宝塔已装但脚本未提权导致 ssh_command_failed;附 ssh_timeout 运维清单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:40:46 +08:00
r 97c47a120f fix(btpanel): 面板登录 URL 使用宝塔 pyenv 执行 tools.py
系统 python3 缺 psutil 导致 SSH 回退失败;相对 tmp_token 自动拼安全入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:16:33 +08:00
r 028dc2c8b6 fix(btpanel): API base_url 不含安全入口路径
宝塔 API 走 :port/system,安全入口仅用于浏览器登录;修复代理 404。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:10:34 +08:00
r 51b6d0cdff fix(btpanel): API 签名改为 md5(time+token)
与面板 common.py 一致;原双重 md5 导致密钥校验失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:05:39 +08:00
r e5929694c3 fix(btpanel): bootstrap 写入 panel/config/api.json
宝塔读取 config/api.json 而非 data/api.json,错误路径导致白名单与 token 不生效。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 10:01:40 +08:00
r b768da3ab8 fix(btpanel): bootstrap 误判 exit_code=0 为失败
Python 中 0 or 1 为 1,导致 SSH 成功仍报 ssh_command_failed。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:52:22 +08:00
r 45ca57bf3f fix(ssh): 连接前从 preset_id 解析 SSH 密码
修复宝塔批量引导因仅关联密码预设、行内 password 为空而全部 ssh_command_failed。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:47:22 +08:00
r 14bbfc4ab7 fix(btpanel): 批量获取时跳过已配置 API 的服务器
前端仅提交未配置 ID;后端 batch 任务对已配置记为跳过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:39:57 +08:00
r 770469c4aa fix(btpanel): 批量勾选提交真实 server id
v-data-table 勾选值为 id 数组,修复误传 undefined 导致 422。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:37:04 +08:00
r b4436143a6 feat(btpanel): 服务器列表批量 SSH 获取 API
支持全部未配置与多选批量引导;未配置 API 时禁用一键登录。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:34:02 +08:00
r 7c99298419 fix(btpanel): 连接配置页始终显示全局设置
未选服务器时不再隐藏整页内容,避免白屏。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:30:03 +08:00
r 461ac81221 fix(btpanel): 接受相对路径 tmp_token 登录链接
宝塔 tools.py 常返回 /login?tmp_token=...,现与 base_url 拼接为完整 URL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:26:57 +08:00
4396 changed files with 90722 additions and 1116 deletions
+15 -1
View File
@@ -2,8 +2,22 @@
"mcpServers": {
"mysql-mcp": {
"command": "bash",
"args": ["scripts/linux_mcp_mysql.sh"],
"args": [
"scripts/linux_mcp_mysql.sh"
],
"env": {}
},
"gitea-mcp": {
"command": "bash",
"args": [
"scripts/mcp_gitea.sh"
],
"env": {
"GITEA_HOST": "https://axs.kuma1xn.vip",
"GITEA_ACCESS_TOKEN_FILE": "/mnt/c/Users/uzuma/.codex/secrets/gitea-axs.token",
"HTTPS_PROXY": "socks5://127.0.0.1:10808",
"HTTP_PROXY": "socks5://127.0.0.1:10808"
}
}
}
}
+6 -1
View File
@@ -38,11 +38,16 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA15 页)· Vite
## 部署
默认 **本机 rsync 直传**(不 push Gitea):
```bash
bash deploy/pre_deploy_check.sh && bash deploy/deploy-production.sh
```
前端: `cd frontend && npx vite build` → tar/scp(见 `deploy/deploy-frontend.sh`
仅同步源码(不重建镜像):`bash deploy/rsync-local-to-server.sh`
仍走 Gitea 拉取:`NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh`
前端: `cd frontend && npx vite build` → 部署脚本内 tar/scp 到 `web/app/`
## 进度条(改代码时输出)
+13
View File
@@ -27,3 +27,16 @@ standards/
web/uploads
backups/
deploy/gate_log.jsonl
# Release/build local artifacts
frontend-v2/node_modules
.venv
.venv-*
venv
.pytest_cache
.ruff_cache
.playwright-mcp
2025.2
=2025.2
*.bak-
tmp/
+1
View File
@@ -16,6 +16,7 @@ deploy/nexus-1panel.secrets.sh
*.pem
.env.*
.venv/
.venv-*/
venv/
ENV/
+10 -6
View File
@@ -19,13 +19,12 @@ cd frontend && npm run dev # :3000/app/
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
## Git本机快照
## Git(本机 + Gitea
- 本地 `git commit` 用于回滚;**不 `git push`**(除非你明确要求)
- 已移除 `origin` 跟踪,避免误推;原远程:`http://66.154.115.8:3000/admin/Nexus.git`(需恢复时说一声)。
- `.env``SECRETS.md``*.pem` 已在 `.gitignore`**勿提交**。
- 改完代码:**`git commit` `bash scripts/git-push.sh` 推到 Gitea**(凭据 `deploy/nexus-1panel.secrets.sh`);大仓库可用 `scripts/git-push-axs-full.sh`
- 本地 commit 仍用于回滚;**不要漏推 Gitea**(用户约定 2026-07-09)。
- `.env``SECRETS.md``*.pem``deploy/nexus-1panel.secrets.sh` 已在 `.gitignore`**勿提交**。
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
- 用户说「提交快照 / commit」时执行。
## 强制约束
@@ -40,11 +39,16 @@ cd frontend && npm run dev # :3000/app/
## 部署
默认 **本机 rsync → SSH 生产机****代码先 push Gitea 再部署**(见上节 Git):
```bash
bash deploy/pre_deploy_check.sh
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
bash scripts/git-push.sh # 或 git-push-axs-full.sh
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
```
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`
详情见功能指南 §1718。
## 归档文档
+272
View File
@@ -0,0 +1,272 @@
#!/usr/bin/env bash
# Apply a Nexus release tarball on the production host.
#
# Usage:
# sudo bash deploy/apply-release-bundle.sh \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1 \
# --deploy-path /opt/nexus
#
# Validate only (no backup, no rsync, no rebuild):
# bash deploy/apply-release-bundle.sh --validate-only \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 6474d58fdccde668b4c91d5e7454fa08e01e20660e3bcbb363eeadbb7c2346a1
#
# This script intentionally preserves runtime secrets and data:
# - .env
# - docker/.env.prod
# - web/data
set -euo pipefail
TARBALL=""
EXPECTED_SHA256=""
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
BACKUP_DIR="${NEXUS_BACKUP_DIR:-/opt/nexus-backups}"
SKIP_REBUILD=0
DRY_RUN=0
VALIDATE_ONLY=0
APPLY_TMP=""
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
usage() {
sed -n '1,26p' "$0"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tarball)
TARBALL="${2:-}"
shift 2
;;
--sha256)
EXPECTED_SHA256="${2:-}"
shift 2
;;
--deploy-path)
DEPLOY_PATH="${2:-}"
shift 2
;;
--backup-dir)
BACKUP_DIR="${2:-}"
shift 2
;;
--skip-rebuild)
SKIP_REBUILD=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--validate-only)
VALIDATE_ONLY=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
error "Unknown argument: $1"
usage
exit 2
;;
esac
done
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
error "Missing required command: $1"
exit 127
}
}
require_runtime_cmds() {
require_cmd tar
require_cmd sha256sum
if [[ "$VALIDATE_ONLY" == 1 ]]; then
return 0
fi
require_cmd rsync
require_cmd curl
}
run() {
echo "+ $*"
if [[ "$DRY_RUN" == 1 ]]; then
return 0
fi
"$@"
}
cleanup() {
if [[ -n "${APPLY_TMP:-}" && -d "$APPLY_TMP" ]]; then
rm -rf "$APPLY_TMP"
fi
}
detect_runtime() {
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
echo docker
return
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo supervisor
return
fi
echo unknown
}
verify_sha256() {
if [[ -z "$EXPECTED_SHA256" ]]; then
warn "No --sha256 provided; skipping checksum verification"
return 0
fi
local actual
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
error "SHA256 mismatch"
error "expected: $EXPECTED_SHA256"
error "actual: $actual"
exit 1
fi
info "SHA256 OK: $actual"
}
validate_extracted_tree() {
local src="$1"
for required in server deploy Dockerfile.prod frontend-v2 web/app-v2; do
if [[ ! -e "${src}/${required}" ]]; then
error "Release bundle missing required path: ${required}"
exit 1
fi
done
for forbidden in .env docker/.env.prod web/data .venv-py312-codex frontend-v2/node_modules 2025.2 =2025.2; do
if [[ -e "${src}/${forbidden}" ]]; then
error "Release bundle contains forbidden path: ${forbidden}"
exit 1
fi
done
info "Extracted release tree validation OK"
}
backup_current() {
if [[ ! -d "$DEPLOY_PATH" ]]; then
warn "Deploy path does not exist yet: $DEPLOY_PATH"
return 0
fi
local stamp backup
stamp="$(date +%Y%m%d-%H%M%S)"
backup="${BACKUP_DIR}/nexus-before-${stamp}.tar.gz"
run mkdir -p "$BACKUP_DIR"
info "Backup current deploy path -> $backup"
run tar czf "$backup" \
--exclude='./web/data' \
--exclude='./.venv*' \
--exclude='./venv' \
--exclude='./frontend/node_modules' \
--exclude='./frontend-v2/node_modules' \
-C "$(dirname "$DEPLOY_PATH")" "$(basename "$DEPLOY_PATH")"
}
sync_release() {
local src="$1"
run mkdir -p "$DEPLOY_PATH"
info "Sync release tree -> $DEPLOY_PATH"
run rsync -a --delete \
--exclude='.env' \
--exclude='docker/.env.prod' \
--exclude='web/data' \
"${src}/" "${DEPLOY_PATH}/"
}
rebuild_or_restart() {
if [[ "$SKIP_REBUILD" == 1 ]]; then
warn "--skip-rebuild set; not rebuilding/restarting Nexus"
return 0
fi
local runtime
runtime="$(detect_runtime)"
info "Runtime: $runtime"
case "$runtime" in
docker)
if [[ -x "${DEPLOY_PATH}/deploy/nexus-1panel.sh" || -f "${DEPLOY_PATH}/deploy/nexus-1panel.sh" ]]; then
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/nexus-1panel.sh" upgrade --skip-git
else
error "Docker runtime detected but deploy/nexus-1panel.sh missing"
exit 1
fi
if [[ -f "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" ]]; then
run env NEXUS_ROOT="$DEPLOY_PATH" bash "${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh"
fi
;;
supervisor)
run supervisorctl restart nexus
;;
*)
error "Unknown runtime. Rebuild/restart manually."
exit 1
;;
esac
}
verify_local_endpoints() {
local port health app app_v2
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
port="${port:-8600}"
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
echo " /health -> ${health}"
echo " /app/ -> ${app}"
echo " /app-v2/ -> ${app_v2}"
if [[ "$health" != "ok" || "$app" != "200" || "$app_v2" != "200" ]]; then
error "Local endpoint verification failed"
exit 1
fi
info "Local endpoint verification OK"
}
main() {
if [[ -z "$TARBALL" ]]; then
error "--tarball is required"
usage
exit 2
fi
[[ -f "$TARBALL" ]] || {
error "Tarball not found: $TARBALL"
exit 1
}
require_runtime_cmds
info "Tarball: $TARBALL"
info "Deploy path: $DEPLOY_PATH"
info "Backup dir: $BACKUP_DIR"
verify_sha256
local extracted
APPLY_TMP="$(mktemp -d /tmp/nexus-release-apply.XXXXXX)"
trap cleanup EXIT
run tar xzf "$TARBALL" -C "$APPLY_TMP"
extracted="${APPLY_TMP}/nexus-release"
validate_extracted_tree "$extracted"
if [[ "$VALIDATE_ONLY" == 1 ]]; then
info "Validate-only mode: tarball checksum and content validation passed"
return 0
fi
backup_current
sync_release "$extracted"
rebuild_or_restart
if [[ "$DRY_RUN" != 1 && "$SKIP_REBUILD" != 1 ]]; then
verify_local_endpoints
fi
info "Release applied successfully"
}
main "$@"
+58 -24
View File
@@ -1,16 +1,19 @@
#!/usr/bin/env bash
# Full production deploy: push (optional) + backend upgrade + frontend + health
# Full production deploy: local rsync + backend upgrade + frontend + health
#
# 自动识别远程 Docker/opt/nexus)或 Supervisor/www/wwwroot/...)运行时
# 默认从本机工作区 rsync 到生产机,不 push Gitea、不远程 git pull
#
# Prerequisites on YOUR machine:
# - SSH: ~/.ssh/config Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
# - rsync, SSH Host nexus OR export NEXUS_SSH="azureuser@20.24.218.235"
# - Key: export NEXUS_SSH_KEY=~/.ssh/id_rsa_nexus
#
# Usage:
# cd "$NEXUS_ROOT"
# bash deploy/pre_deploy_check.sh
# bash deploy/deploy-production.sh
#
# 仍走 Gitea(仅当显式设置):
# NEXUS_DEPLOY_VIA_GIT=1 bash deploy/deploy-production.sh
set -euo pipefail
@@ -31,8 +34,15 @@ fi
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
scp_cmd() { scp "${SSH_OPTS[@]}" "$@"; }
DEPLOY_VIA_GIT="${NEXUS_DEPLOY_VIA_GIT:-0}"
case "${DEPLOY_VIA_GIT}" in
1|true|yes|on) DEPLOY_VIA_GIT=1 ;;
*) DEPLOY_VIA_GIT=0 ;;
esac
echo "═══ Nexus production deploy ═══"
echo "SSH target: ${NEXUS_SSH_HOST}"
echo "Mode: $([[ "$DEPLOY_VIA_GIT" == 1 ]] && echo 'Gitea git pull' || echo 'local rsync')"
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
@@ -94,37 +104,61 @@ fi
echo "Deploy path: ${DEPLOY_PATH}"
echo "Runtime: ${RUNTIME}"
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Docker: git pull + 镜像重建(前端在镜像内 vite build..."
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
elif [[ "$RUNTIME" == "supervisor" ]]; then
echo "▶ Supervisor: git pull + restart..."
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
resolve_deploy_chown() {
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
echo "${NEXUS_DEPLOY_CHOWN}"
return
fi
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
echo "www:www"
return
fi
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
}
DEPLOY_CHOWN="$(resolve_deploy_chown)"
echo "File owner: ${DEPLOY_CHOWN}"
echo "▶ Frontend: build + upload..."
cd frontend && npx vite build && cd ..
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
ssh_cmd "cd ${DEPLOY_PATH}/web/app && tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py --dry-run"
ssh_cmd "python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"
if [[ "$DEPLOY_VIA_GIT" == 1 ]]; then
echo "▶ Git mode: remote git pull..."
if [[ "$RUNTIME" == "docker" ]]; then
ssh_cmd "cd ${DEPLOY_PATH} && sudo git fetch --all && sudo git reset --hard origin/main && \
sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade"
elif [[ "$RUNTIME" == "supervisor" ]]; then
ssh_cmd "cd ${DEPLOY_PATH} && git fetch --all && git reset --hard origin/main && supervisorctl restart nexus"
else
echo "ERROR: 无法识别远程运行时" >&2
exit 1
fi
else
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor" >&2
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
exit 1
echo "▶ Local sync → ${DEPLOY_PATH}..."
NEXUS_DEPLOY_PATH="${DEPLOY_PATH}" bash "${ROOT}/deploy/rsync-local-to-server.sh"
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Docker: 重建镜像(--skip-git..."
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} bash ${DEPLOY_PATH}/deploy/nexus-1panel.sh upgrade --skip-git"
elif [[ "$RUNTIME" == "supervisor" ]]; then
echo "▶ Supervisor: restart..."
ssh_cmd "supervisorctl restart nexus"
else
echo "ERROR: 无法识别远程运行时(非 Docker 也非 Supervisor" >&2
echo " 请手动: ssh ${NEXUS_SSH_HOST} 'sudo nx update'" >&2
exit 1
fi
fi
if [[ "$RUNTIME" == "docker" ]]; then
# Host web/app must match image build before sync_webapp_to_container (else stale assets overwrite container).
if [[ "$RUNTIME" == "docker" || "$RUNTIME" == "supervisor" ]]; then
echo "▶ Frontend: local vite build + upload to host web/app..."
cd frontend && npx vite build && cd ..
tar czf /tmp/nexus-frontend.tar.gz -C web/app index.html assets/
scp_cmd /tmp/nexus-frontend.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend.tar.gz"
ssh_cmd "cd ${DEPLOY_PATH}/web/app && sudo tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz"
ssh_cmd "cd ${DEPLOY_PATH}/web/app && sudo tar xzf /tmp/nexus-frontend.tar.gz && rm /tmp/nexus-frontend.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app"
if ssh_cmd "test -f ${DEPLOY_PATH}/deploy/prune_frontend_assets.py"; then
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app --dry-run"
ssh_cmd "sudo python3 ${DEPLOY_PATH}/deploy/prune_frontend_assets.py ${DEPLOY_PATH}/web/app"
fi
fi
if [[ "$RUNTIME" == "docker" ]]; then
echo "▶ Sync host web/app into container..."
ssh_cmd "sudo env NEXUS_ROOT=${DEPLOY_PATH} NEXUS_PUBLISH_PORT=\$(grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod 2>/dev/null | cut -d= -f2 | tr -d '\"' || echo 8600) bash ${DEPLOY_PATH}/deploy/sync_webapp_to_container.sh" || {
echo "WARN: web/app sync failed — run sync_webapp_to_container.sh on server manually" >&2
+604
View File
@@ -2378,3 +2378,607 @@
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"changelog","result":"PASS","detail":"2026-06-18-terminal-persist-navigation.md 40lines"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-18T12:37:07+08:00","date":"2026-06-18","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"security","result":"BLOCK","detail":"2 HIGH findings"}
{"ts":"2026-06-21T08:09:43+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"security","result":"BLOCK","detail":"2 HIGH findings"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"shell_eol","result":"BLOCK","detail":"CR in tracked sh"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:12:56+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"2/8"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:14:10+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T08:14:26+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md 29lines"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-login-allowlist-deploy.md"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:13:54+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-deploy.md 30lines"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-ssh-bootstrap.md"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:14:50+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-deploy.md 32lines"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-ssh-bootstrap.md"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T09:19:48+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 33lines"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:09:06+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"6/8"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 33lines"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:09:38+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 38lines"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:17:16+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md 38lines"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-login-auto-bootstrap.md"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:17:29+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-btpanel-nav-hidden.md 23lines"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-btpanel-nav-hidden.md"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:22:14+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-file-transfer-audit-fixes.md 35lines"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-file-transfer.md"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:48:54+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-file-transfer-audit-fixes.md 35lines"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-file-transfer.md"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:49:05+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-nav-watch-metrics-under-system.md 23lines"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T12:59:50+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-server-picker-fix.md 24lines"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:05:39+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-target-path-default.md 28lines"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:09:48+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-dest-browser.md 20lines"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-page.md"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:15:01+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:19:34+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"audit","result":"BLOCK","detail":"missing sections"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:19:43+08:00","date":"2026-06-21","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-source-site-link.md 17lines"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:20:03+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 39lines"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:25:39+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 47lines"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:28:40+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-deploy-local-rsync.md 48lines"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:35:37+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-manual-dest.md 31lines"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T13:40:17+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-dest-write-elevation.md 31lines"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T14:46:10+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-curl-tmp-staging.md 29lines"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:07:14+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-btpanel-download-tar-overwrite.md 39lines"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:28:20+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"changelog","result":"PASS","detail":"2026-06-21-server-transfer-btpanel-download-tar-overwrite.md 43lines"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"audit","result":"PASS","detail":"2026-06-21-server-transfer-ui-polish.md"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-21T15:32:54+08:00","date":"2026-06-21","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-22T05:47:49+08:00","date":"2026-06-22","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"changelog","result":"PASS","detail":"2026-06-22-btpanel-temp-login-24h.md 30lines"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"audit","result":"PASS","detail":"2026-06-22-btpanel-temp-login-24h.md"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-22T05:48:15+08:00","date":"2026-06-22","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-subscription-ip-refresh-primary-fix.md 36lines"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T10:39:14+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"4/8"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:06:19+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"4/8"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-23T21:06:32+08:00","date":"2026-06-23","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"audit","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:09:02+08:00","date":"2026-06-23","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"changelog","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md 43lines"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"audit","result":"PASS","detail":"2026-06-23-detect-path-nginx-root.md"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-23T21:09:24+08:00","date":"2026-06-23","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:11:18+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"5/8"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:11:57+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md 42lines"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T02:12:07+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:04:07+08:00","date":"2026-06-24","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:04:27+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"changelog","result":"PASS","detail":"2026-06-24-servers-unset-path-table-align.md 77lines"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"audit","result":"PASS","detail":"2026-06-24-ip-domain-detect-schedule.md"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"test","result":"PASS","detail":"all passed"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-24T15:08:09+08:00","date":"2026-06-24","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-06-29T18:22:15+08:00","date":"2026-06-29","gate":"summary","result":"BLOCK","detail":"3/8"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"changelog","result":"BLOCK","detail":"file not found"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"audit","result":"BLOCK","detail":"file not found"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"test","result":"BLOCK","detail":"tests failed"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"review","result":"BLOCK","detail":"no audit file"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:03:53+08:00","date":"2026-07-01","gate":"summary","result":"BLOCK","detail":"3/8"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:09:17+08:00","date":"2026-07-01","gate":"summary","result":"BLOCK","detail":"7/8"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:09:49+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md 54lines"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-gate-catchup-terminal-btpanel.md"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T12:10:11+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"changelog","result":"PASS","detail":"2026-07-01-agent-watch-dual-mode.md 38lines"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"audit","result":"PASS","detail":"2026-07-01-agent-watch-dual-mode.md"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-01T18:29:45+08:00","date":"2026-07-01","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-agent-cpu-sampling.md 35lines"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-agent-cpu-sampling.md"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:20:36+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:36:42+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T00:47:58+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T02:45:32+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T03:10:05+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"changelog","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md 29lines"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"audit","result":"PASS","detail":"2026-07-02-stats-unset-path-offline-card.md"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"ai_review","result":"PASS","detail":"skipped no code"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-02T03:13:21+08:00","date":"2026-07-02","gate":"summary","result":"PASS","detail":"8/8"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"changelog","result":"PASS","detail":"2026-07-03-terminal-btpanel-login-button.md 29lines"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"audit","result":"PASS","detail":"2026-07-03-terminal-btpanel-login-button.md"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"test","result":"PASS","detail":"pytest fallback :8600 down"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"lint","result":"PASS","detail":"0 violations"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"import","result":"PASS","detail":"ok"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"security","result":"PASS","detail":"0 HIGH"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"review","result":"PASS","detail":"audit covers changes"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"ai_review","result":"PASS","detail":"ok"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"text_eol","result":"PASS","detail":"git index LF"}
{"ts":"2026-07-03T23:19:41+08:00","date":"2026-07-03","gate":"summary","result":"PASS","detail":"8/8"}
+1
View File
@@ -2,6 +2,7 @@
# 用于 git pushscripts/git-push.sh);clone/pull 仍可匿名
# NEXUS_GITEA_HOST='66.154.115.8:3000'
# NEXUS_GITEA_HOST='axs.kuma1xn.vip' # HTTPS Gitea2026-07-08 起,分支 release/20260708-axs
# NEXUS_GITEA_REPO='admin/Nexus.git'
# admin 账号密码或 Access Token
# NEXUS_GITEA_TOKEN='你的密码或令牌'
+33 -8
View File
@@ -23,7 +23,7 @@ set -euo pipefail
# 内置路径/域名(无敏感信息,可提交 Git)
# ═══════════════════════════════════════════════════════════════════
NEXUS_ROOT="${NEXUS_ROOT:-/opt/nexus}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-66.154.115.8:3000}"
NEXUS_GITEA_HOST="${NEXUS_GITEA_HOST:-axs.kuma1xn.vip}"
NEXUS_GITEA_REPO="${NEXUS_GITEA_REPO:-admin/Nexus.git}"
NEXUS_DOMAIN="${NEXUS_DOMAIN:-api.synaglobal.vip}"
NEXUS_PROFILE="${NEXUS_PROFILE:-2c8g}"
@@ -50,6 +50,7 @@ error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "${CYAN}[STEP]${NC} $*"; }
SKIP_CLONE=false
SKIP_GIT=false
FRESH_INSTALL=false
REUSE_SECRETS=false
FROM_ENV="${NEXUS_FROM_ENV}"
@@ -181,9 +182,13 @@ apply_pool_to_env_prod() {
local root="$1" prof="$2"
local env_file="$root/$ENV_PROD"
[[ -f "$env_file" ]] || return 0
if [[ ! -f "$prof" ]]; then
warn "未找到资源档位文件: $prof,保留 $ENV_PROD 中现有数据库连接池配置"
return 0
fi
local pool overflow
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2-)"
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2-)"
pool="$(grep -E '^NEXUS_DB_POOL_SIZE=' "$prof" | cut -d= -f2- || true)"
overflow="$(grep -E '^NEXUS_DB_MAX_OVERFLOW=' "$prof" | cut -d= -f2- || true)"
[[ -n "$pool" ]] && sed -i "s|^NEXUS_DB_POOL_SIZE=.*|NEXUS_DB_POOL_SIZE=${pool}|" "$env_file"
[[ -n "$overflow" ]] && sed -i "s|^NEXUS_DB_MAX_OVERFLOW=.*|NEXUS_DB_MAX_OVERFLOW=${overflow}|" "$env_file"
}
@@ -725,10 +730,14 @@ require_token() {
}
git_remote_url() {
local scheme=http
if [[ "$NEXUS_GITEA_HOST" != *:* ]]; then
scheme="${NEXUS_GITEA_SCHEME:-https}"
fi
if [[ -n "${NEXUS_GITEA_TOKEN:-}" ]]; then
echo "http://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
echo "${scheme}://admin:${NEXUS_GITEA_TOKEN}@${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
else
echo "http://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
echo "${scheme}://${NEXUS_GITEA_HOST}/${NEXUS_GITEA_REPO}"
fi
}
@@ -1018,10 +1027,14 @@ verify_health() {
}
ensure_repo() {
if [[ ! -d "$NEXUS_ROOT/.git" ]]; then
if [[ ! -d "$NEXUS_ROOT" ]]; then
error "未找到 $NEXUS_ROOT,请先: bash $0 install"
exit 1
fi
if [[ "$SKIP_GIT" != true && ! -d "$NEXUS_ROOT/.git" ]]; then
error "未找到 $NEXUS_ROOT/.git,请先: bash $0 install;如为发布包/rsync 部署请使用 --skip-git"
exit 1
fi
if [[ ! -f "$NEXUS_ROOT/$ENV_PROD" ]]; then
error "缺少 $NEXUS_ROOT/$ENV_PROD"
exit 1
@@ -1138,8 +1151,12 @@ cmd_upgrade() {
load_saved_profile "$NEXUS_ROOT"
resolve_profile
check_ports_preflight "$NEXUS_ROOT"
git_fetch
show_pending || true
if [[ "$SKIP_GIT" == true ]]; then
info "跳过 Git 同步(本机 rsync 已推送代码)"
else
git_fetch
show_pending || true
fi
if [[ "$CHECK_ONLY" == true ]]; then
exit 0
@@ -1149,6 +1166,12 @@ cmd_upgrade() {
exit 0
fi
if [[ "$SKIP_GIT" == true ]]; then
info "本机直传模式:跳过 git reset,仍备份并重建镜像"
if [[ "$NO_BACKUP" != true ]]; then
backup_mysql "$NEXUS_ROOT"
fi
else
local behind
behind="$(commits_behind)"
if [[ "$behind" != "0" ]]; then
@@ -1160,6 +1183,7 @@ cmd_upgrade() {
backup_mysql "$NEXUS_ROOT"
fi
fi
fi
apply_pool_to_env_prod "$NEXUS_ROOT" "$(profile_env_file "$NEXUS_ROOT")"
upsert_env_var "$NEXUS_ROOT/$ENV_PROD" "NEXUS_HOST_ROOT" "$NEXUS_ROOT"
@@ -1272,6 +1296,7 @@ parse_common_upgrade() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
--skip-git) SKIP_GIT=true; shift ;;
--profile) NEXUS_PROFILE="$2"; shift 2 ;;
--cpus) CUSTOM_CPUS="$2"; shift 2 ;;
--mem-gb|--memory-gb) CUSTOM_MEM_GB="$2"; shift 2 ;;
+32 -5
View File
@@ -131,12 +131,39 @@ else
TEST_OUTPUT=$(cd "${DEPLOY_DIR}" && env -u NEXUS_TEST_BASE NEXUS_TEST_BASE=http://127.0.0.1:8600 python3 "${TEST_SCRIPT}" 2>&1) || true
FAIL_COUNT=$(echo "${TEST_OUTPUT}" | grep -cE "^\s+\[FAIL\]" 2>/dev/null || true)
FAIL_COUNT=${FAIL_COUNT:-0}
GATE3_PASSED=0
if [ "${FAIL_COUNT}" -gt 0 ] || echo "${TEST_OUTPUT}" | grep -qE "[0-9]+ test(s) failed"; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Tests failed. Output:"
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
BLOCKED=1
gate_log "test" "BLOCK" "tests failed"
# 本机未起 :8600 时,若失败均为 Connection refused,回退 pytest 专项
NON_CONN_FAILS=$(echo "${TEST_OUTPUT}" | grep -E "^\s+\[FAIL\]" | grep -vcE "Connection refused|Errno 111" 2>/dev/null || true)
NON_CONN_FAILS=${NON_CONN_FAILS:-0}
if [ "${FAIL_COUNT}" -gt 0 ] && [ "${NON_CONN_FAILS}" -eq 0 ]; then
PYTEST_BIN=$(_pick_bin pytest || true)
FALLBACK_TESTS="tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py tests/test_gate_ai_review.py"
if [ -n "${PYTEST_BIN}" ]; then
echo -e "${YELLOW}fallback${NC}"
echo " └─ Local :8600 unreachable — running pytest gate batch"
FALLBACK_OUTPUT=$(cd "${DEPLOY_DIR}" && "${PYTEST_BIN}" ${FALLBACK_TESTS} -q 2>&1) || FALLBACK_RC=$?
FALLBACK_RC=${FALLBACK_RC:-0}
if [ "${FALLBACK_RC}" -eq 0 ]; then
echo -e " └─ ${GREEN}PASS${NC} (pytest fallback)"
GATES_PASSED=$((GATES_PASSED + 1))
gate_log "test" "PASS" "pytest fallback :8600 down"
GATE3_PASSED=1
else
echo -e " └─ ${RED}BLOCK${NC} pytest fallback failed:"
echo "${FALLBACK_OUTPUT}" | tail -15 | sed 's/^/ │ /'
BLOCKED=1
gate_log "test" "BLOCK" "pytest fallback failed"
fi
fi
fi
if [ "${GATE3_PASSED}" -eq 0 ]; then
echo -e "${RED}BLOCK${NC}"
echo " └─ Tests failed. Output:"
echo "${TEST_OUTPUT}" | head -20 | sed 's/^/ │ /'
BLOCKED=1
gate_log "test" "BLOCK" "tests failed"
fi
else
echo -e "${GREEN}PASS${NC}"
echo " └─ All tests passed"
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env bash
# Nexus production host preflight before applying a release tarball.
#
# Usage:
# bash deploy/preflight-release-host.sh \
# --tarball /tmp/nexus-release-20260708.tar.gz \
# --sha256 a4eb8f35f0b98d40c1ad83117bfc652e7686ffc7d55f6bf90fcfd433a2ad6701 \
# --deploy-path /opt/nexus
set -euo pipefail
TARBALL=""
EXPECTED_SHA256=""
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-/opt/nexus}"
MIN_FREE_MB="${NEXUS_RELEASE_MIN_FREE_MB:-1024}"
info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
usage() {
sed -n '1,12p' "$0"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--tarball)
TARBALL="${2:-}"
shift 2
;;
--sha256)
EXPECTED_SHA256="${2:-}"
shift 2
;;
--deploy-path)
DEPLOY_PATH="${2:-}"
shift 2
;;
--min-free-mb)
MIN_FREE_MB="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
error "Unknown argument: $1"
usage
exit 2
;;
esac
done
check_cmd() {
local cmd="$1"
if command -v "$cmd" >/dev/null 2>&1; then
info "command OK: $cmd"
return 0
fi
error "missing command: $cmd"
return 1
}
docker_cmd() {
if docker "$@" 2>/dev/null; then return 0; fi
sudo docker "$@" 2>/dev/null
}
detect_runtime() {
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]] && command -v docker >/dev/null 2>&1; then
echo docker
return
fi
if command -v supervisorctl >/dev/null 2>&1 && supervisorctl status nexus >/dev/null 2>&1; then
echo supervisor
return
fi
echo unknown
}
verify_tarball() {
if [[ -z "$TARBALL" ]]; then
warn "--tarball not provided; skipping tarball checksum/content checks"
return 0
fi
[[ -f "$TARBALL" ]] || {
error "tarball not found: $TARBALL"
return 1
}
info "tarball exists: $TARBALL"
if [[ -n "$EXPECTED_SHA256" ]]; then
local actual
actual="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ "$actual" != "$EXPECTED_SHA256" ]]; then
error "SHA256 mismatch: expected=$EXPECTED_SHA256 actual=$actual"
return 1
fi
info "SHA256 OK: $actual"
else
warn "--sha256 not provided; checksum not verified"
fi
tar tzf "$TARBALL" nexus-release/server nexus-release/deploy nexus-release/web/app-v2 >/dev/null
info "tarball required paths OK"
}
check_deploy_path() {
if [[ -d "$DEPLOY_PATH" ]]; then
info "deploy path exists: $DEPLOY_PATH"
else
warn "deploy path does not exist yet: $DEPLOY_PATH"
fi
if [[ -f "${DEPLOY_PATH}/.env" ]]; then
info "runtime .env preserved path exists"
else
warn "${DEPLOY_PATH}/.env not found"
fi
if [[ -f "${DEPLOY_PATH}/docker/.env.prod" ]]; then
info "docker/.env.prod exists"
else
warn "${DEPLOY_PATH}/docker/.env.prod not found"
fi
if [[ -d "${DEPLOY_PATH}/web/data" ]]; then
info "web/data exists"
else
warn "${DEPLOY_PATH}/web/data not found"
fi
}
check_disk_space() {
local target free_mb
target="$DEPLOY_PATH"
[[ -e "$target" ]] || target="$(dirname "$DEPLOY_PATH")"
free_mb="$(df -Pm "$target" | awk 'NR==2 {print $4}')"
if [[ -z "$free_mb" ]]; then
warn "unable to determine free disk space for $target"
return 0
fi
if (( free_mb < MIN_FREE_MB )); then
error "low disk space on $target: ${free_mb}MB < ${MIN_FREE_MB}MB"
return 1
fi
info "disk free OK: ${free_mb}MB >= ${MIN_FREE_MB}MB"
}
check_runtime() {
local runtime
runtime="$(detect_runtime)"
info "runtime detected: $runtime"
case "$runtime" in
docker)
docker_cmd ps --format '{{.Names}}' | grep -E 'nexus-prod-nexus|nexus-nexus-1' >/dev/null \
&& info "Nexus docker container found" \
|| warn "Nexus docker container not found by known name pattern"
;;
supervisor)
supervisorctl status nexus || true
;;
*)
warn "runtime unknown; apply script may require manual restart"
;;
esac
}
check_local_endpoint() {
local port health app app_v2
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "${DEPLOY_PATH}/docker/.env.prod" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
port="${port:-8600}"
health="$(curl -s "http://127.0.0.1:${port}/health" 2>/dev/null || true)"
app="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app/" 2>/dev/null || echo 000)"
app_v2="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${port}/app-v2/" 2>/dev/null || echo 000)"
echo " current /health -> ${health:-<empty>}"
echo " current /app/ -> ${app}"
echo " current /app-v2/ -> ${app_v2}"
}
main() {
local failures=0
for cmd in tar sha256sum df awk grep sed curl; do
check_cmd "$cmd" || failures=$((failures + 1))
done
check_cmd rsync || failures=$((failures + 1))
verify_tarball || failures=$((failures + 1))
check_deploy_path
check_disk_space || failures=$((failures + 1))
check_runtime
check_local_endpoint
if (( failures > 0 )); then
error "preflight failed: ${failures} blocking issue(s)"
return 1
fi
info "preflight OK"
}
main "$@"
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Sync local Nexus working tree to production host (no Gitea push/pull).
# Transfer via tar|ssh; remote ownership auto-detected (www:www on BT, else deploy path owner).
#
# Usage:
# bash deploy/rsync-local-to-server.sh
# NEXUS_DEPLOY_PATH=/opt/nexus bash deploy/rsync-local-to-server.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT}"
SECRETS="${ROOT}/deploy/nexus-1panel.secrets.sh"
if [[ -f "$SECRETS" ]]; then
# shellcheck disable=SC1090
source "$SECRETS"
fi
NEXUS_SSH_HOST="${NEXUS_SSH:-nexus}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=15)
if [[ -n "${NEXUS_SSH_KEY:-}" ]]; then
SSH_OPTS+=(-i "${NEXUS_SSH_KEY}")
fi
ssh_cmd() { ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "$@"; }
if ! ssh_cmd "echo SSH OK" >/dev/null 2>&1; then
echo "ERROR: Cannot SSH to ${NEXUS_SSH_HOST}" >&2
exit 1
fi
DEPLOY_PATH="${NEXUS_DEPLOY_PATH:-}"
if [[ -z "$DEPLOY_PATH" ]]; then
DEPLOY_PATH="$(ssh_cmd 'for d in /opt/nexus /www/wwwroot/api.synaglobal.vip; do [[ -d "$d/server" ]] && echo "$d" && exit 0; done; echo /opt/nexus')"
fi
resolve_deploy_chown() {
if [[ -n "${NEXUS_DEPLOY_CHOWN:-}" ]]; then
echo "${NEXUS_DEPLOY_CHOWN}"
return
fi
if ssh_cmd "getent passwd www >/dev/null 2>&1"; then
echo "www:www"
return
fi
ssh_cmd "stat -c '%U:%G' '${DEPLOY_PATH}'" 2>/dev/null || echo "azureuser:azureuser"
}
DEPLOY_CHOWN="$(resolve_deploy_chown)"
echo "═══ Nexus local → server sync ═══"
echo "SSH target: ${NEXUS_SSH_HOST}"
echo "Remote path: ${DEPLOY_PATH}"
echo "Owner: ${DEPLOY_CHOWN}"
TAR_EXCLUDES=(
--exclude='.git'
--exclude='.env'
--exclude='.env.*'
--exclude='SECRETS.md'
--exclude='deploy/nexus-1panel.secrets.sh'
--exclude='docker/.env.prod'
--exclude='web/data'
--exclude='frontend/node_modules'
--exclude='node_modules'
--exclude='.venv'
--exclude='venv'
--exclude='__pycache__'
--exclude='.cursor'
--exclude='.install_locked'
--exclude='.install_state.json'
--exclude='web/app/assets'
--exclude='tmp'
--exclude='frontend/test-results'
--exclude='frontend/e2e/.auth'
)
echo "▶ tar stream → ${DEPLOY_PATH} ..."
tar -C "${ROOT}" -czf - "${TAR_EXCLUDES[@]}" . | \
ssh "${SSH_OPTS[@]}" "${NEXUS_SSH_HOST}" "sudo tar -xzf - -C '${DEPLOY_PATH}'"
echo "▶ chown ${DEPLOY_CHOWN} (skip docker/.env.prod, web/data, .env) ..."
ssh_cmd "sudo bash -s" <<REMOTE
set -euo pipefail
root='${DEPLOY_PATH}'
own='${DEPLOY_CHOWN}'
chown_tree() {
local p="\$1"
[[ -e "\$p" ]] || return 0
sudo chown -R "\$own" "\$p"
}
for top in server deploy scripts tests standards docs frontend web; do
chown_tree "\$root/\$top"
done
if [[ -d "\$root/docker" ]]; then
while IFS= read -r -d '' p; do
sudo chown -R "\$own" "\$p"
done < <(find "\$root/docker" \( -path "\$root/docker/.env.prod" \) -prune -o -print0)
fi
for f in pyproject.toml requirements.txt requirements-dev.txt AGENTS.md CLAUDE.md; do
[[ -f "\$root/\$f" ]] && sudo chown "\$own" "\$root/\$f"
done
REMOTE
echo "✓ sync complete (owner ${DEPLOY_CHOWN})"
@@ -0,0 +1,48 @@
# 审计 — 2026-06-21 宝塔一键登录自动 bootstrap
**Changelog**: `docs/changelog/2026-06-21-btpanel-login-auto-bootstrap.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `btpanel_service.py` `create_login_url` | 未配置先 bootstrap,失败显式 ValueError;审计含 bootstrapped | PASS |
| `useBtPanelLogin.ts` | 无静默吞错,成功提示区分 bootstrap | PASS |
| `BtPanelLoginPage.vue` | 移除未配置拦截,与 servers 页一致 | PASS |
| `bootstrap_server` | 复用既有锁与 immediate 源,无重复 SSH 逻辑 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 未配置仅 SSH 登录 | FIXED | login-url 先 immediate bootstrap |
| 前端需手动获取 API | FIXED | 一键登录单请求完成 |
| 宝塔页拦截未配置 | FIXED | 与 servers 行为统一 |
## 文件清单
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/client.py`
- `frontend/src/App.vue`
- `frontend/src/api/btpanel.ts`
- `frontend/src/types/global.d.ts`
- `frontend/src/composables/btpanel/useBtPanelLogin.ts`
- `frontend/src/composables/btpanel/useBtPanelBatchBootstrap.ts`
- `frontend/src/components/btpanel/BtPanelBatchBootstrapDialog.vue`
- `frontend/src/components/servers/ServerBatchActionBar.vue`
- `frontend/src/components/servers/ServerUnsetPathPanel.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
- `frontend/src/pages/btpanel/BtPanelMonitorPage.vue`
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_client.py`
- `tests/test_btpanel_login_url_route.py`
- `server/api/btpanel.py`
- `docs/changelog/2026-06-21-btpanel-login-auto-bootstrap.md`
## DoD
- [x] `#/servers` 一键登录未配置时自动获取 API 后打开面板
- [x] bootstrap 失败返回可读错误
- [x] `pytest tests/test_btpanel_login_url.py` 通过
- [x] changelog 已写
@@ -0,0 +1,26 @@
# 审计 — 2026-06-21 侧栏隐藏宝塔菜单
**Changelog**: `docs/changelog/2026-06-21-btpanel-nav-hidden.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `App.vue` | 仅移除导航 UI,路由与 API 未删 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 侧栏宝塔入口 | DONE | 整组菜单隐藏 |
| servers 一键登录 | OK | 未改动 |
## 文件清单
- `frontend/src/App.vue`
- `docs/changelog/2026-06-21-btpanel-nav-hidden.md`
## DoD
- [x] 侧栏无「宝塔面板」分组
- [x] changelog 已写
@@ -0,0 +1,30 @@
# 审计 — 2026-06-21 宝塔临时登录 24 小时
**Changelog**: `docs/changelog/2026-06-21-btpanel-temp-login-24h.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `constants.py` | 单点 TTL 常量 | PASS |
| `btpanel_service.py` | API 传 expire_timetoken 拼 URL | PASS |
| `ssh_login.py` | base64 远程脚本,无静默吞错 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 默认 3h 临时登录 | FIXED | API + SSH 均 86400s |
## 文件清单
- `server/infrastructure/btpanel/constants.py`
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/ssh_login.py`
- `tests/test_btpanel_temp_login_ttl.py`
- `docs/changelog/2026-06-21-btpanel-temp-login-24h.md`
## DoD
- [x] expire_time = now + 86400
- [x] pytest 通过
@@ -0,0 +1,46 @@
# 审计 — 2026-06-21 跨服务器文件传输
**Changelog**: `docs/changelog/2026-06-21-server-file-transfer.md``docs/changelog/2026-06-21-server-file-transfer-audit-fixes.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `server_file_transfer_service.py` | 路径 `shlex.quote`、直传 500MB 上限、deliver 失败回滚 | PASS |
| `transfer-download` | token Redis TTLJWT 前缀白名单 | PASS(已知:前缀对所有 method 生效) |
| `useFilesActions.ts` | 模式推荐基于 `transferItems`relay/deliver 分路径缓存 | PASS |
| `test_server_file_transfer.py` | deliver 成功/回滚/超限、relay、store | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| deliver 失败泄漏归档/token | FIXED | rollback cleanup |
| deliver 成功 token 残留 | FIXED | delete_package_meta |
| 右键传输模式误判 | FIXED | transferItems |
| 直传多项中途失败无回滚 | ACCEPT | v1 文档化 |
## 文件清单
- `server/application/services/server_file_transfer_service.py`
- `server/infrastructure/redis/transfer_package_store.py`
- `server/api/sync_v2.py`
- `server/api/schemas.py`
- `server/api/auth_jwt.py`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/composables/files/useFilesPage.ts`
- `frontend/src/components/files/FilesDialogs.vue`
- `frontend/src/components/files/FilesList.vue`
- `frontend/src/components/files/FilesToolbar.vue`
- `frontend/src/utils/auditLabels.ts`
- `tests/test_server_file_transfer.py`
- `docs/design/specs/2026-06-21-server-file-transfer-design.md`
- `docs/design/plans/2026-06-21-server-file-transfer.md`
- `docs/changelog/2026-06-21-server-file-transfer.md`
- `docs/changelog/2026-06-21-server-file-transfer-audit-fixes.md`
## DoD
- [x] 直传 + 打包投递 API 与前端对话框
- [x] `pytest tests/test_server_file_transfer.py` 18 passed
- [x] changelog ≥10 行
@@ -0,0 +1,41 @@
# 审计 — 2026-06-21 跨服务器传输独立页 + 侧栏调整
**Changelog**: `docs/changelog/2026-06-21-server-transfer-page.md``docs/changelog/2026-06-21-nav-watch-metrics-under-system.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `useServerFileTransfer.ts` | 路径 normalize、relay 500MB 禁用、localStorage 分模式缓存 | PASS |
| `useFilesActions.ts` | 移除内嵌传输;跳转 query 预填 | PASS |
| `ServerTransferPage.vue` | 顶层解构 ref,模板类型正确 | PASS |
| `App.vue` | 仅菜单分组调整,路由不变 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 文件页内嵌传输对话框 | REMOVED | 改独立页 |
| 监测历史菜单位置 | MOVED | 运维 → 系统 |
## 文件清单
- `frontend/src/pages/ServerTransferPage.vue`
- `frontend/src/composables/useServerFileTransfer.ts`
- `frontend/src/router/index.ts`
- `frontend/src/App.vue`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/composables/files/useFilesPage.ts`
- `frontend/src/components/files/FilesDialogs.vue`
- `frontend/src/components/files/FilesToolbar.vue`
- `docs/project/nexus-functional-development-guide.md`
- `docs/changelog/2026-06-21-server-transfer-page.md`
- `docs/changelog/2026-06-21-nav-watch-metrics-under-system.md`
## DoD
- [x] 独立页 `#/server-transfer` + 侧栏入口
- [x] 文件页跳转预填 query
- [x] 监测历史归入系统分组
- [x] `npx vite build` 通过
- [x] changelog ≥10 行
@@ -0,0 +1,32 @@
# 审计 — 2026-06-21 跨服务器传输 UI 补强
**Changelog**: `docs/changelog/2026-06-21-server-transfer-dest-browser.md``docs/changelog/2026-06-21-server-transfer-source-site-link.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `useServerFileTransfer.ts` | B 机 browse 同步 destPath;站点 URL 计算 | PASS |
| `ServerTransferPage.vue` | 双端文件表;A 机站点外链 `rel=noopener` | PASS |
| `useServerList.ts` | ServerBrief 扩展 extra_attrs 供站点解析 | PASS |
## 文件清单
- `frontend/src/composables/useServerFileTransfer.ts`
- `frontend/src/composables/useServerList.ts`
- `frontend/src/pages/ServerTransferPage.vue`
- `docs/changelog/2026-06-21-server-transfer-dest-browser.md`
- `docs/changelog/2026-06-21-server-transfer-source-site-link.md`
## Closure
| 项 | 状态 |
|----|------|
| B 机目录浏览 | DONE |
| A 机站点链接 | DONE |
## DoD
- [x] B 机文件列表与路径同步
- [x] A 机下方可点击站点 URL
- [x] `vite build` 通过
@@ -0,0 +1,30 @@
# 审计 — 2026-06-22 宝塔临时登录 24 小时
**Changelog**: `docs/changelog/2026-06-22-btpanel-temp-login-24h.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `constants.py` | 单点 TTL 常量 | PASS |
| `btpanel_service.py` | API 传 expire_timetoken 拼 URL | PASS |
| `ssh_login.py` | base64 远程脚本,无静默吞错 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 默认 3h 临时登录 | FIXED | API + SSH 均 86400s |
## 文件清单
- `server/infrastructure/btpanel/constants.py`
- `server/application/services/btpanel_service.py`
- `server/infrastructure/btpanel/ssh_login.py`
- `tests/test_btpanel_temp_login_ttl.py`
- `docs/changelog/2026-06-22-btpanel-temp-login-24h.md`
## DoD
- [x] expire_time = now + 86400
- [x] pytest 通过
@@ -0,0 +1,46 @@
# 审计 — 检测路径改为 nginx 网站目录
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/utils/nginx_path_detect.py` | 新增:构建远程 nginx root 检测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_target_path` 改用 nginx |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
| `tests/test_server_onboarding.py` | mock 错误文案 |
| `server/application/services/btpanel_service.py` | 同批部署:宝塔临时登录 TTL |
| `server/infrastructure/btpanel/constants.py` | 同上 |
| `server/infrastructure/btpanel/ssh_login.py` | 同上 |
| `tests/test_btpanel_temp_login_ttl.py` | 同上 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 远程命令 domain 须 `shlex.quote` | PASS |
| H2 | 解析路径走 `normalize_remote_abs_path` | PASS |
| H3 | 非 root SSH 仍 `sudo_wrap` | PASS |
| H4 | 未静默吞 SSH 错误 | PASS — 无 stdout 返回明确错误 |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `nginx_path_detect.py` `shlex.quote(host)` |
| H2 | SAFE | `server_batch_common.py` normalize 后写入 |
| H3 | SAFE | 沿用既有 `sudo_wrap` |
| H4 | SAFE | `未找到 nginx 网站目录` |
## DoD
- [x] pytest tests/test_nginx_path_detect.py
- [x] changelog 2026-06-23-detect-path-nginx-root.md
- [x] 功能指南 § 检测目标路径 已更新
## 验证
```bash
pytest tests/test_nginx_path_detect.py tests/test_server_onboarding.py -q
```
@@ -0,0 +1,53 @@
# 审计 — IP-only 服务器自动补全站点域名
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/utils/site_host.py` | 新增:IP 判定与站点 host 解析 |
| `server/utils/nginx_domain_detect.py` | 新增:远程 nginx 域名探测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_site_url` / `update_server_site_url` |
| `server/application/services/ip_domain_detect_schedule.py` | 新增:23:30 定时筛选与触发 |
| `server/background/ip_domain_detect_loop.py` | 新增:background loop |
| `server/application/services/server_batch_service.py` | batch op `detect-domain` |
| `server/config.py` | `IP_DOMAIN_DETECT_*` 配置 |
| `server/main.py` | 注册 loop |
| `tests/test_site_host.py` | 单元测试 |
| `tests/test_nginx_domain_detect.py` | 单元测试 |
| `tests/test_ip_domain_detect_schedule.py` | 单元测试 |
| `server/application/services/btpanel_service.py` | 同批未提交:宝塔临时登录 TTL |
| `server/infrastructure/btpanel/constants.py` | 同上 |
| `server/infrastructure/btpanel/ssh_login.py` | 同上 |
| `tests/test_btpanel_temp_login_ttl.py` | 同上 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 远程 shell 参数须 `shlex.quote` | PASS — `target_hint` 已 quote |
| H2 | 不改 SSH `domain` 字段 | PASS — 仅写 `extra_attrs.site_url` |
| H3 | 非 root SSH 仍 `sudo_wrap` | PASS — 沿用 detect-path 模式 |
| H4 | 未静默吞 SSH 错误 | PASS — 无 stdout 返回明确错误 |
| H5 | 每日 Redis 去重防重复 batch | PASS — `already_ran_today` |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `nginx_domain_detect.py` `shlex.quote(hint)` |
| H2 | SAFE | `update_server_site_url` 只 merge extra_attrs |
| H3 | SAFE | `detect_and_save_site_url` 调用 `sudo_wrap` |
| H4 | SAFE | `未找到 nginx 站点域名` |
| H5 | SAFE | `ip_domain_detect_schedule.py` Redis key |
## DoD
- [x] pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py
- [x] changelog 2026-06-24-ip-domain-detect-schedule.md
- [x] 设计文档 specs + plans
## 验证
```bash
.venv/bin/pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py -q
```
@@ -0,0 +1,31 @@
# 审计 — 2026-06-29 宝塔 bootstrap 跳过无意义 reload
**Changelog**: `docs/changelog/2026-06-29-btpanel-bootstrap-skip-reload.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `ssh_bootstrap.py` | 有变更才 reload;仍原子写 api.json | PASS |
| `test_btpanel_ssh_bootstrap.py` | 远程脚本含 `if actions:` 先于 reload | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 重复 bootstrap 踢面板会话 | FIXED | `actions` 空则跳过写盘与 reload |
| 首次对接须 reload | OK | 新 token / 开 API / 加白名单仍 reload |
## 文件清单
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `docs/design/specs/2026-06-20-btpanel-module-design.md`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次改动对齐
- [x] `pytest tests/test_btpanel_ssh_bootstrap.py` 16 passed
- [x] 生产 rsync + Docker upgrade
@@ -0,0 +1,44 @@
# 审计 — 每日离线巡检发报前 SSH 复检
## 范围(文件清单)
| 文件 | 变更 |
|------|------|
| `server/application/server_connectivity.py` | `health_check_server_ids`;扫描列表 `batch_server_display_name` |
| `server/application/services/offline_daily_report_schedule.py` | preflight 后二次 snapshot |
| `server/config.py` | `OFFLINE_DAILY_REPORT_HEALTH_CHECK` |
| `server/infrastructure/telegram/__init__.py` | `preflight` 参数与文案 |
| `server/background/server_offline_monitor.py` | 告警显示名备注优先 |
| `tests/test_offline_daily_report_schedule.py` | preflight 单测 |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 复用既有 SSH 探测,不新造协议 | PASS — `ssh_health_probe` + `write_ssh_health_heartbeat` |
| H2 | 并发受控 | PASS — `Semaphore(10)` 与批量健康检查一致 |
| H3 | 可配置关闭 | PASS — `OFFLINE_DAILY_REPORT_HEALTH_CHECK` |
| H4 | 日报数据为复检后 snapshot | PASS — 二次 `collect_offline_snapshot` |
| H5 | 显示名与批量任务 SSOT 一致 | PASS — `batch_server_display_name` |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1 | SAFE | `server_connectivity.health_check_server_ids` |
| H2 | SAFE | `asyncio.Semaphore(max(1, concurrency))` |
| H3 | SAFE | `_health_check_enabled()` |
| H4 | SAFE | `offline_daily_report_schedule.run_scheduled_offline_daily_report` |
| H5 | SAFE | `scan_monitored_connectivity` / `server_offline_monitor` |
## DoD
- [x] pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py
- [x] changelog 2026-06-30-offline-daily-preflight-health-check.md
- [x] 无 DB 迁移
## 验证
```bash
.venv/bin/pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py -q
```
@@ -0,0 +1,41 @@
# 审计 — Agent 双模式四槽监测
**Changelog**: `docs/changelog/2026-07-01-agent-watch-dual-mode.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `agent.py` (子机) | watch 模式每 5s 全量;退出 watch 回 60s | PASS |
| `agent.py` (中心) | `watch_active` 仅查 monitoring pin 计数 | PASS |
| `watch_probe_runner` | 仅 `watch_mode`+新鲜心跳跳过 SSH | PASS |
| 进程列表 | 仍 SSH 偶发拉取 | PASS(已知限制) |
## Closure
| 项 | 状态 |
|----|------|
| 四槽长期 SSH 风控 | 有 Agent 时改走出站 5s |
| 双 Agent 并存 | 不需要,单进程双模式 |
| 旧 Agent 兼容 | SSH 兜底 |
## 文件清单
- `web/agent/agent.py`
- `web/agent/config.example.json`
- `server/api/agent.py`
- `server/background/watch_probe_runner.py`
- `server/utils/watch_metrics.py`
- `tests/test_agent_watch_mode.py`
## DoD
- [x] pytest 19 passedwatch + agent_watch_mode
- [x] 心跳响应含 watch_active
- [x] Agent 2.1.0 watch_mode 字段
## 验证
```bash
.venv/bin/pytest tests/test_agent_watch_mode.py tests/test_watch_metrics.py -q
```
@@ -0,0 +1,53 @@
# 审计 — 2026-07-01 补录门禁(宝塔 / Terminal / 快捷命令)
**Changelog**: `docs/changelog/2026-07-01-gate-catchup-terminal-btpanel.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `ssh_bootstrap.py` | 无变更不写盘、不 reload | PASS |
| `terminal.py` | 静态路由 `reorder` 先于 `{id}` | PASS |
| `useServerQuickAdd.ts` | 复用 Servers `add-by-ip` 契约 | PASS |
| `ServerQuickAddDialogs.vue` | 与 Servers 弹窗一致 | PASS |
| `TerminalPage.vue` | 成功后 `loadServers` + `newSession` | PASS |
| `TerminalQuickCommandsSettings.vue` | `@click.stop` 防列表吞事件 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 重复 bootstrap 踢宝塔会话 | FIXED | `if actions:` 才 reload |
| reorder 被 `{id}` 抢路由 | FIXED | 路由顺序调整 + 单测 |
| Terminal 无快速添加 | FIXED | TabBar 按钮 + 共用对话框 |
| 6/30 部署跳过门禁 | 补录 | 本 changelog/audit + pre_deploy |
## 文件清单
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/api/terminal.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_terminal_quick_commands.py`
- `frontend/src/composables/servers/useServerQuickAdd.ts`
- `frontend/src/components/servers/ServerQuickAddDialogs.vue`
- `frontend/src/utils/openServerTerminal.ts`
- `frontend/src/components/terminal/TerminalTabBar.vue`
- `frontend/src/pages/TerminalPage.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/TerminalQuickCommandsSettings.vue`
- `deploy/pre_deploy_check.sh`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次批次一致
- [x] `pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py` 通过
- [x] `pre_deploy_check.sh` 全门通过(Gate 3 回退或本机 :8600
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py -q
bash deploy/pre_deploy_check.sh
```
@@ -0,0 +1,39 @@
# 审计 — Agent 2.1.1 CPU 采样
**Changelog**: `docs/changelog/2026-07-02-agent-cpu-sampling.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `cpu_metrics.py` | watch 首次 1s bootstrap,后续 interval=None 对齐 ~5s 心跳 | PASS |
| `agent.py` | 进入 watch 时 `reset_watch_cpu_bootstrap()` | PASS |
| `server_batch_common` | 升级命令含 cpu_metrics.py | PASS |
| 常态 60s 心跳 | 仍用 `sample_cpu_percent(watch_mode=False)` | PASS |
## Closure
| 项 | 状态 |
|----|------|
| CPU 曲线长期 0% | 监测模式改长窗口采样 |
| 双模心跳 | 不变,仍 5s/60s |
| 旧 Agent 2.1.0 | 可继续用,仅 CPU 显示偏 0 |
## 文件清单
- `web/agent/cpu_metrics.py`
- `web/agent/agent.py`
- `server/application/server_batch_common.py`
- `tests/test_agent_cpu_metrics.py`
## DoD
- [x] pytest cpu_metrics + watch_mode 通过
- [x] AGENT_VERSION 2.1.1
- [x] 升级脚本拉取 cpu_metrics.py
## 验证
```bash
.venv/bin/pytest tests/test_agent_cpu_metrics.py tests/test_agent_watch_mode.py -q
```
@@ -0,0 +1,39 @@
# 审计 — 统计卡「未设路径·离线」
**Changelog**: `docs/changelog/2026-07-02-stats-unset-path-offline-card.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `servers.py` | `unset_path_offline` 由 fleet/main 离线差值推导 | PASS |
| `useServerStatsCards.ts` | 6 卡顺序与字段映射 | PASS |
| `ServersPage.vue` | 点击展开未设路径 + 离线筛 | PASS |
| `DashboardPage.vue` | 跳转 query 一致 | PASS |
## Closure
| 项 | 状态 |
|----|------|
| 在线+离线≠总数 | 拆出未设路径·离线后可对齐 |
| 主列表离线筛选 | 不变,仍 8 台口径 |
## 文件清单
- `server/api/servers.py`
- `frontend/src/composables/useServerStatsCards.ts`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/DashboardPage.vue`
- `tests/integration/test_servers_dashboard.py`
## DoD
- [x] API 返回 `unset_path_offline`
- [x] 前后端 6 卡展示
- [x] integration test 断言差值公式
## 验证
```bash
.venv/bin/pytest tests/integration/test_servers_dashboard.py -q
```
@@ -0,0 +1,39 @@
# 审计 — 2026-07-03 Terminal 宝塔登录按钮
**Changelog**: `docs/changelog/2026-07-03-terminal-btpanel-login-button.md`
## Step 3 规则扫描
| 文件 | 规则 | 结论 |
|------|------|------|
| `TerminalToolbar.vue` | 仅 `session` 存在时显示;loading 防重复点击 | PASS |
| `TerminalPage.vue` | 复用 `useBtPanelLogin``serverId` 取自 `activeSession` | PASS |
| `useBtPanelLogin.ts` | 未改;并发去重与 `window.open` 行为不变 | PASS |
## Closure
| 项 | 状态 | 说明 |
|----|------|------|
| 终端内无法一键进宝塔 | FIXED | 工具栏「宝塔登录」+ 移动端菜单 |
| 与列表登录行为不一致 | N/A | 同一 composable / API |
## 文件清单
- `frontend/src/components/terminal/TerminalToolbar.vue`
- `frontend/src/pages/TerminalPage.vue`
## DoD
- [x] Step 3 规则扫描
- [x] Closure 表
- [x] 文件清单与本次批次一致
- [x] `npm run build-only` 通过
- [x] `pre_deploy_check.sh` 全门通过
## 验证
```bash
cd frontend && npm run build-only
bash deploy/pre_deploy_check.sh
bash deploy/deploy-frontend.sh
```
@@ -0,0 +1,40 @@
# 终端页切换路由保持 SSH 会话
**日期**2026-06-18
## 摘要
用户从「终端」页导航至其他 SPA 页面时,WebSSH 会话不再被主动断开;返回终端页可继续操作同一标签与会话。
## 动机
此前 `TerminalPage` 被刻意排除在 `keep-alive` 之外,且 `onBeforeUnmount` 中调用 `disposeAllSessions()`,路由切换即卸载组件并关闭全部 WebSocket/SSH 连接,与运维「开终端查问题、切页看列表、再回来继续敲命令」的工作流冲突。
## 方案
1. 将 `TerminalPage` 加入 `CACHED_PAGE_NAMES`,由 `App.vue` 现有 `<keep-alive>` 缓存实例。
2. 生命周期调整:
- `onDeactivated`:仅移除全局快捷键监听,**不** dispose 会话。
- `onActivated`:恢复快捷键、refit xterm、聚焦当前标签。
- `onBeforeUnmount`:仅在缓存淘汰或组件真正销毁时 dispose(与 `keep-alive max=12` 一致)。
- 监听 `auth.isLoggedIn`:登出时 dispose,避免缓存中残留 SSH 连接。
未将会话提升到全局 store`termNativeStore` 已是模块级单例,配合 keep-alive 即可,改动最小。
## 涉及文件
- `frontend/src/constants/cachedPages.ts`
- `frontend/src/pages/TerminalPage.vue`
## 迁移 / 重启
无。仅前端构建/热更新。
## 验证
```bash
cd frontend && npm run build
pytest tests/test_webssh_terminal_payload.py tests/test_terminal_quick_commands.py -q
```
手动:登录 → 终端连接一台服务器 → 侧栏切「服务器」→ 再切回「终端」→ 会话仍 connected,可继续输入;登出后重进终端应为空会话。
@@ -0,0 +1,27 @@
# 2026-06-21 宝塔 bootstrap 写入正确 api.json 路径
## 摘要
SSH bootstrap 将 API 配置写入 `/www/server/panel/data/api.json`,而宝塔面板实际读取 `/www/server/panel/config/api.json`,导致白名单与 token 未生效,所有代理 API 返回「IP校验失败」。
## 动机
生产验收:8 台已 configured 服务器全部代理失败;子机 exec 显示 `config/api.json` 白名单仍为旧 IP `172.96.253.26`,与 `data/api.json` 不一致。
## 涉及文件
- `server/infrastructure/btpanel/client.py` — 签名 `md5(time+token)`
- `server/infrastructure/btpanel/ssh_bootstrap.py``base_url` 不含安全入口路径
- `tests/test_btpanel_ssh_bootstrap.py``exit_code=0` 回归用例(上一轮)
## 迁移 / 重启
- 需部署 API 后,对已 configured 服务器**重新执行 bootstrap**(手动或批量)以合并白名单与 token 到正确文件。
- 无需 DB 迁移。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py -q
# 生产:POST /api/btpanel/servers/{id}/bootstrap 后 GET .../system/total 应返回真实指标而非 IP 错误
```
@@ -0,0 +1,29 @@
# Changelog — 宝塔批量 SSH 获取 API2026-06-21
**日期**2026-06-21
## 摘要
服务器列表页增加批量/全量「SSH 获取 API」;未配置 API 时禁用一键登录(避免 404 临时链)。
## 动机
- 2000+ 子机需先写入 `api.json` 才能稳定一键登录
- 原批量入口仅在连接配置页,不易发现
- `all_unconfigured` 服务端解析未配置列表,突破请求体 500 台上限
## 涉及文件
- `server/api/btpanel_schemas.py``all_unconfigured`
- `server/application/services/btpanel_service.py`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
- `frontend/src/pages/btpanel/BtPanelSettingsPage.vue`
- `frontend/src/api/btpanel.ts`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_*.py -q
cd frontend && npx vite build
# 服务器列表 → 全部未配置 / 勾选 → 查看批量任务进度
```
@@ -0,0 +1,30 @@
# 宝塔批量获取 API 并入服务器页批量栏
**日期**2026-06-21
## 变更摘要
将宝塔「全部未配置」「选中获取 API」移至 `#/servers` 批量操作栏「宝塔」分组;抽取 `useBtPanelBatchBootstrap` 与确认对话框组件。宝塔子模块列表页移除顶部重复按钮。
## 动机
运维在服务器主列表即可完成宝塔 SSH 引导,无需先切到宝塔 · 服务器列表。
## 涉及文件
- `frontend/src/composables/btpanel/useBtPanelBatchBootstrap.ts`
- `frontend/src/components/btpanel/BtPanelBatchBootstrapDialog.vue`
- `frontend/src/components/servers/ServerBatchActionBar.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/servers/ServerUnsetPathPanel.vue`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
## 迁移 / 重启
仅前端构建部署。
## 验证
1. `#/servers` 勾选服务器 → 批量栏「宝塔」见全部未配置 / 选中获取 API
2. 确认对话框启动 job,任务中心可查看进度
3. 宝塔 · 服务器列表页无顶部批量按钮,一键登录仍可用
@@ -0,0 +1,28 @@
# 2026-06-21 宝塔 bootstrap 非 root SSH 自动 sudo
## 摘要
ubuntu 等非 root SSH 用户(如 `preset_id=2`)可连机且宝塔已装,但 bootstrap 脚本未提权导致读 `port.pl` 失败,批量报 `ssh_command_failed`。现对 bootstrap、宝塔检测、面板地址探测、临时登录 SSH 统一 `sudo -n bash -c`
## 动机
生产抽样 #135/#136/#147SSH✅、`sudo -n`✅、root 下宝塔✅,非 sudo bootstrap 崩溃。
## 涉及文件
- `server/infrastructure/btpanel/ssh_bootstrap.py``wrap_sudo_nopasswd``ssh_sudo_required`
- `server/infrastructure/btpanel/ssh_login.py` — 检测/探测/登录命令提权
- `tests/test_btpanel_ssh_bootstrap.py`
- `docs/reports/2026-06-21-btpanel-ssh-timeout-servers.md` — 超时机器运维清单
## 迁移 / 重启
- 部署 API 后对未配置服务器重跑批量 bootstrap。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py tests/test_btpanel_ssh_login.py -q
POST /api/btpanel/servers/135/bootstrap # ubuntu 抽样
POST /api/btpanel/servers/batch-bootstrap {"all_unconfigured": true}
```
@@ -0,0 +1,38 @@
# 宝塔一键登录:未配置时自动获取 API
**日期**2026-06-21
## 摘要
`#/servers` 与宝塔连接配置页的「一键登录」在未获取宝塔 API 时,会先通过 SSH bootstrap 写入 `api.json`,再生成临时登录链接;无需用户先点「获取 API」。
## 动机
行内一键登录应一步完成;此前未配置机器仅走 SSH 临时登录或需手动 bootstrap,与批量栏「选中获取 API」能力不一致。
## 涉及文件
- `server/application/services/btpanel_service.py``create_login_url` 未配置时 `source=immediate` bootstrap
- `frontend/src/composables/btpanel/useBtPanelLogin.ts` — 未配置时顶部 snackbar 20s 倒计时(`timeout: -1` 持久显示)
- `frontend/src/App.vue` — snackbar 支持自定义 `timeout`
- `frontend/src/api/btpanel.ts` — 响应类型含 `bootstrapped`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue` — 移除未配置拦截,bootstrap 后刷新列表
- `tests/test_btpanel_login_url.py`
## 迁移 / 重启
无 schema 变更;需部署 API 与前端静态资源。
## 验证
```bash
pytest tests/test_btpanel_login_url.py -q
cd frontend && npx vite build
```
生产:对未配置宝塔 API 的子机点「一键登录」,应自动获取后打开面板;审计 `bt_panel_login_url` 的 detail 含 `bootstrapped: true`
## 2026-06-21 热修
- **根因**`login-url` 路由返回类型 `dict[str, str]`,响应含 `bootstrapped: bool` → FastAPI 校验 500
- **修复**`server/api/btpanel.py` 改为 `dict[str, Any]`
@@ -0,0 +1,21 @@
# 2026-06-21 宝塔一键登录 URL 路径修复
## 摘要
临时登录链接误拼为 `https://ip:port/{安全入口}/login?tmp_token=...`,宝塔实际入口为 `https://ip:port/login?tmp_token=...`(带安全入口路径返回 nginx 404)。
## 动机
生产 curl`/1f78dd53/login?tmp_token=...` → 404`/login?tmp_token=...` → 302。
## 涉及文件
- `server/infrastructure/btpanel/ssh_login.py``_panel_port_base``_login_path_only``_discover_panel_login_base_url`
- `tests/test_btpanel_ssh_login.py`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_login.py -q
POST /api/btpanel/servers/3/login-url # url 应为 ...:port/login?tmp_token=
```
@@ -0,0 +1,26 @@
# 2026-06-21 宝塔面板登录 URL 修复
## 摘要
修复「面板登录」`POST /api/btpanel/servers/{id}/login-url` 返回 500:SSH 回退改用宝塔自带 `pyenv` 执行 `tools.py`,相对路径自动探测含安全入口的浏览器地址。
## 动机
生产验收:系统 `python3``tools.py``psutil``get_temp_login_ipv4` 输出多行(裸 base + `/login?tmp_token=`),需解析并拼安全入口。
## 涉及文件
- `server/infrastructure/btpanel/ssh_login.py` — pyenv Python、多行解析、`resolve_temp_login_url`
- `server/application/services/btpanel_service.py` — API `set_temp_login` 相对路径走面板地址探测
- `tests/test_btpanel_ssh_login.py`
## 迁移 / 重启
- 部署 API 即可;无需 DB 迁移。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_login.py -q
# 生产:POST /api/btpanel/servers/3/login-url → 200url 含 tmp_token 与安全入口
```
@@ -0,0 +1,34 @@
# 2026-06-21 宝塔系统监控 IP 白名单自愈
## 摘要
修复 `#/btpanel/monitor`(及全部宝塔代理 API)在子机返回 `{"status":false,"msg":"IP校验失败..."}` 时被当作正常 JSON 展示的问题;遇 IP 白名单失败时自动 SSH 追加中心 IP 并重试一次。
## 动机
生产验收:`GET /api/btpanel/servers/{id}/system/total` 对部分已 configured 服务器返回 HTTP 200 + 宝塔错误 JSON,前端「系统概览」卡片直接显示 `IP校验失败` 对象而非指标。根因:
1. 历史 bootstrap 曾写入 `data/api.json`,面板实际读 `config/api.json`,白名单未生效;
2. `BtPanelClient` 未识别宝塔 `status: false` 语义。
手动 `POST .../bootstrap` 可修复(`appended_whitelist`),但需用户逐台操作。
## 涉及文件
- `server/infrastructure/btpanel/client.py``raise_if_panel_error`
- `server/application/services/btpanel_service.py``proxy` IP 失败自愈 + `_repair_ip_whitelist`
- `frontend/src/pages/btpanel/BtPanelMonitorPage.vue` — 失败时清空卡片并 snackbar
- `tests/test_btpanel_client.py`
## 迁移 / 重启
- 需部署 API;无需 DB 迁移。
- 已 configured 且 SSH 可达的服务器:首次访问监控/站点等代理接口时自动补白名单。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_client.py -q
# 生产:对曾报 IP 校验失败的服务器 GET .../system/total 应返回 memTotal/cpuNum 等指标
# 前端:#/btpanel/monitor 选服务器后应显示指标 JSON,失败时红色 snackbar 而非假数据
```
@@ -0,0 +1,23 @@
# 侧栏隐藏宝塔面板菜单
**日期**2026-06-21
## 摘要
左侧导航移除「宝塔面板」分组及全部子菜单;宝塔能力仍通过 `#/servers` 一键登录与批量获取 API 使用,`#/btpanel/*` 路由保留(直链可访问)。
## 动机
运维入口收敛到服务器页,避免侧栏重复暴露未常用宝塔管理页。
## 涉及文件
- `frontend/src/App.vue`
## 迁移 / 重启
仅前端静态资源;`cd frontend && npx vite build` 后同步 `web/app/`
## 验证
登录后侧栏无「宝塔面板」区块;`#/servers` 一键登录与批量栏「宝塔」分组正常。
@@ -0,0 +1,19 @@
# 2026-06-21 宝塔服务器列表增加在线状态列
## 摘要
`GET /api/btpanel/servers` 与「宝塔 · 服务器列表」表格新增 **状态** 列(在线 / 离线 / 未知),与主服务器列表一致,优先读 Redis 心跳。
## 涉及文件
- `server/application/services/btpanel_service.py`
- `frontend/src/api/btpanel.ts`
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
- `tests/test_btpanel_server_list.py`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_server_list.py -q
# 前端:宝塔 → 服务器列表,状态列显示在线/离线
```
@@ -0,0 +1,24 @@
# Changelog — 宝塔连接配置页空白修复(2026-06-21)
**日期**2026-06-21
## 摘要
修复「连接配置」在未选服务器时整页几乎空白:全局设置(中心 IP、自动获取、批量初始化)现始终可见。
## 动机
`BtPanelPageShell` 原先要求先选服务器才渲染插槽,全局配置被挡在门外,用户看到白屏或仅一条提示。
## 涉及文件
- `frontend/src/components/btpanel/BtPanelPageShell.vue` — 新增 `requireServer`(默认 true
- `frontend/src/pages/btpanel/BtPanelSettingsPage.vue``require-server=false`,单机块按选中状态展示
## 迁移 / 重启
- 仅前端
## 验证
打开 `#/btpanel/settings`:应立刻看到「全局」卡片;选服务器后显示「当前服务器」表单。
@@ -0,0 +1,30 @@
# 宝塔一键登录临时时效改为 24 小时
**日期**2026-06-21
## 摘要
一键登录生成临时 `tmp_token` 时,`expire_time` 设为当前时间 + **86400 秒**24 小时);SSH 回退不再调用 `tools.py get_temp_login_ipv4`(固定 3 小时),改为远程执行同等逻辑并传入 TTL。
## 动机
用户希望登录后面板会话与临时链接上限为 24 小时;宝塔默认 3 小时,且 `tmp_login_expire` 为生成链接时的绝对时间。
## 涉及文件
- `server/infrastructure/btpanel/constants.py``BT_TEMP_LOGIN_TTL_SECONDS = 86400`
- `server/application/services/btpanel_service.py` — API `set_temp_login``expire_time`,解析 `token` 拼 URL
- `server/infrastructure/btpanel/ssh_login.py``build_ssh_temp_login_command`
- `tests/test_btpanel_temp_login_ttl.py`
## 迁移 / 重启
仅 API 变更;部署后端镜像即可,前端无改动。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_login.py -q
```
生产:`POST /api/btpanel/servers/{id}/login-url` 后登录,约 24 小时内不应提示「临时登录已过期」(仍受面板 `session_timeout.pl` 约束)。
@@ -0,0 +1,28 @@
# Changelog — 宝塔临时登录相对路径修复(2026-06-21)
**日期**2026-06-21
## 摘要
修复 `tools.py get_temp_login_ipv4` / `set_temp_login` 返回 `/login?tmp_token=...` 相对路径时被误判为无效链接的问题。
## 动机
部分宝塔版本 SSH 输出不含协议与主机,原正则仅接受 `https://.../login?tmp_token=...`,导致一键登录报错。
## 涉及文件
- `server/infrastructure/btpanel/ssh_login.py``normalize_temp_login_url`、无 base_url 时 SSH 探测面板地址
- `server/infrastructure/btpanel/credentials.py``get_bt_panel_base_url`
- `server/application/services/btpanel_service.py` — API/SSH 登录统一规范化
- `tests/test_btpanel_ssh_login.py`
## 迁移 / 重启
- 需重启 API 容器
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_login.py -q
```
@@ -0,0 +1,48 @@
# 部署:本机 rsync 直传(默认不走 Gitea
**日期**2026-06-21
## 摘要
生产部署默认从本机工作区 `rsync` 到服务器,远程 `nexus-1panel.sh upgrade --skip-git` 重建镜像;不再依赖 `git push` + 远程 `git pull`
## 动机
用户要求平时不推 Gitea;部署时直接同步本机已验证代码即可。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `deploy/rsync-local-to-server.sh` | 新建:rsync 到 `/opt/nexus`,排除 `.env` / `docker/.env.prod` / `web/data` |
| `deploy/deploy-production.sh` | 默认 rsync`NEXUS_DEPLOY_VIA_GIT=1` 可恢复旧流程 |
| `deploy/nexus-1panel.sh` | `upgrade --skip-git` 跳过 git fetch/reset |
| `AGENTS.md``.cursorrules` | 部署说明更新 |
## 用法
```bash
bash deploy/pre_deploy_check.sh
bash deploy/deploy-production.sh
```
## 保护项(不同步/不改属主)
- `docker/.env.prod``web/data/``.env`、secrets
## 属主
- **子机跨传**`NEXUS_RSYNC_PUSH_CHOWN`(默认 `www:www`),见 `docs/changelog/2026-06-21-server-transfer-www-permissions.md`
- **中心机部署**:有 `www` 用户则 `www:www`,否则用部署目录现有属主(Azure 为 `azureuser:azureuser`);`NEXUS_DEPLOY_CHOWN` 可覆盖
## 传输方式
本机 `tar | ssh sudo tar -x`(不依赖远程 rsync 命令)。
## 验证
`bash -n deploy/*.sh`;用户批准后执行完整 deploy 并检查 `/health``/app/`
## 回滚
服务器保留 Docker 回滚镜像;或本机 checkout 旧版再 rsync 部署。
@@ -0,0 +1,31 @@
# 健康检查写入 Redis 心跳
**日期**2026-06-21
## 变更摘要
批量「健康检查」在 SSH 探测后,将结果写入 `heartbeat:{server_id}`(与 Agent 心跳同结构),列表/宝塔模块可立即反映在线状态;任务结果 stdout 附带心跳时间。
## 动机
健康检查仅返回成功/失败,不更新 Redis,导致 `#/servers` 状态列与宝塔列表仍显示旧心跳或 unknown。
## 涉及文件
- `server/application/server_connectivity.py``write_ssh_health_heartbeat``normalize_ssh_probe_system_info`
- `server/application/services/server_batch_service.py``_run_health_check` 写心跳
- `server/application/services/server_service.py``check_all_servers` 同步写心跳
- `server/api/servers.py``has_heartbeat` 时无 Agent 也显示 offline/online
- `tests/test_server_connectivity.py``tests/test_server_status_field.py`
## 迁移 / 重启
需重启 API worker(后端部署)。
## 验证方式
```bash
.venv/bin/pytest tests/test_server_connectivity.py tests/test_server_status_field.py -q
```
生产:对离线机执行健康检查后刷新 `#/servers`,状态列应更新;批量任务详情 stdout 含「心跳 2026-…」。
@@ -0,0 +1,23 @@
# 侧栏:监测历史归入系统分组
**日期**2026-06-21
## 摘要
将「监测历史」从侧栏「运维」分组移至「系统」分组(位于告警中心与审计日志之间)。
## 动机
监测历史属于系统观测/留存类功能,与命令日志、告警、审计同属系统侧栏更合理。
## 涉及文件
- `frontend/src/App.vue``opsItems` / `sysItems` 调整
## 迁移 / 重启
仅前端静态资源;路由 `#/watch-metrics` 不变。
## 验证
刷新 SPA,确认侧栏「系统」下可见「监测历史」,「运维」下已无该项。
@@ -0,0 +1,35 @@
# 跨服务器文件传输 — 审计修复
## 日期
2026-06-21
## 摘要
对跨机传输首版做走读审计,修复 deliver 失败资源泄漏、token 残留、前端模式误判与路径记忆混用。
## 审计发现与处置
| 级别 | 问题 | 处置 |
|------|------|------|
| P1 | `deliver` 拉取失败后源机 `/tmp` 归档与 Redis token 未清理 | 失败时 rollback`rm` 归档 + `delete_package_meta` |
| P1 | `deliver` 成功后 token 仍有效但源归档已删,curl 重放 404 | 成功后 `delete_package_meta` |
| P2 | 右键「发送到其他服务器」未选中行时,模式推荐误用 `selectedFiles` | 引入 `transferItems`,按实际传输项判断 |
| P2 | localStorage 直传目录与打包路径共用 key,切换模式路径错乱 | 按 `relay`/`deliver` 分 key |
| P2 | 打包成功立即关对话框,`download_url` 无法复制 | 投递成功后保留对话框至用户关闭 |
| 已知限制 | 直传多项中途失败无回滚 | v1 接受;UI 已提示大目录用打包 |
| 已知限制 | 归档与直传单文件 500MB 上限 | 与现有 download 一致,未改 |
## 涉及文件
- `server/application/services/server_file_transfer_service.py`
- `frontend/src/composables/files/useFilesActions.ts`
- `frontend/src/components/files/FilesDialogs.vue`
## 验证
```bash
.venv/bin/pytest tests/test_server_file_transfer.py -v
```
**18** 项:schema、JWT 白名单、Redis store、SFTP 大小守卫、deliver 成功/失败回滚/超限、relay 同源拒绝与成功路径、`cleanup` 命令引用。
@@ -0,0 +1,44 @@
# 跨服务器文件传输
## 日期
2026-06-21
## 摘要
文件管理器新增 A→B 跨机传输:**直传**(中心 SFTP 中继,单文件 ≤500MB)与 **打包投递**(源机 tar.gz → 签名下载 URL → 目标机 curl + 可选解压,一键完成)。
## 动机
同机剪贴板无法跨服务器;运维需对标宝塔「发送到服务器 / 打包下载」能力,避免本地下载再上传。
## 涉及文件
- `server/application/services/server_file_transfer_service.py` — 直传、打包、投递编排
- `server/infrastructure/redis/transfer_package_store.py` — 下载 token 元数据
- `server/api/sync_v2.py``server-transfer``server-transfer/deliver``transfer-download`
- `server/api/schemas.py` — 请求模型
- `server/api/auth_jwt.py` — 下载端点 JWT 豁免
- `frontend/src/composables/files/useFilesActions.ts` — 对话框与 API 调用
- `frontend/src/components/files/FilesDialogs.vue``FilesToolbar.vue``FilesList.vue`
- `frontend/src/utils/auditLabels.ts`
- `tests/test_server_file_transfer.py`
- `docs/design/specs/2026-06-21-server-file-transfer-design.md`
## 迁移 / 重启
- 需重启 API(新路由 + Redis key `transfer:pkg:*`
- 无需 DB 迁移
- 前端需 `vite build` 后部署 `web/app/`
## 验证
```bash
pytest tests/test_server_file_transfer.py -q
cd frontend && npm run type-check
bash scripts/local_verify.sh
```
- 文件页选中项 →「发送到其他服务器」→ 直传 / 打包投递
- `GET /api/sync/transfer-download/{token}` 无 JWT 可 curl;过期 404
- 审计动作:`server_file_relay``server_file_deliver`
@@ -0,0 +1,27 @@
# 服务器列表默认按创建时间倒序
**日期**2026-06-21
## 变更摘要
服务器列表、推送/终端全量拉取默认 `sort_by=created_at&sort_order=desc`;后端 `server_repo` 无排序参数时同样默认最新在前。
## 动机
新机入库后应出现在列表顶部,便于运维发现与操作。
## 涉及文件
- `server/infrastructure/database/server_repo.py`
- `frontend/src/composables/useServerPagination.ts`
- `frontend/src/composables/push/usePushServers.ts`
- `frontend/src/composables/terminal/useTerminalSessions.ts`
- `frontend/src/utils/serverTableSort.ts`
## 迁移 / 重启
需 API 重启(后端排序逻辑)。
## 验证
打开 `#/servers`,新加服务器应排在前列;终端/推送页服务器下拉顺序一致。
@@ -0,0 +1,43 @@
# 打包投递:tar 覆盖解压 + 宝塔签名下载通道
**日期**2026-06-21
## 摘要
1. 解压 tar 时加 `--overwrite`,修复 `404.html: Cannot open: File exists`
2. 源机已配置宝塔 API 时,打包投递改走宝塔官方 **`/download?filename=…` 签名 URL**(B 机直拉 A 机面板),不经 Nexus 中转;效果等同自动化「下载」,**不是** UI 右键分享外链(该接口无公开 API)。
## 动机
- 站点目录已有文件时默认 tar 拒绝覆盖。
- 用户希望用宝塔下载能力;分享 `/down/{token}` 无 token API,签名 `/download` 为可自动化等价方案。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/infrastructure/btpanel/file_urls.py` | `signed_download_url` |
| `server/application/services/server_file_transfer_service.py` | `_extract_archive_on_server``_deliver_via_btpanel_download` |
| `tests/test_btpanel_file_urls.py` | 签名 URL 单测 |
| `tests/test_server_file_transfer.py` | 覆盖解压 / 宝塔投递分支 |
## 前提
- 源机 `extra_attrs.bt_panel` 已配置且 API IP 白名单允许 **B 机公网 IP** 访问面板端口。
- 未配置宝塔 API 时仍走 Nexus `transfer-download` 原流程。
## 验证
```bash
pytest tests/test_btpanel_file_urls.py tests/test_server_file_transfer.py -q
```
打包投递至已有文件的站点目录,应覆盖成功。
## 回滚
去掉 `bt_panel` 分支与 `--overwrite`,重启 API。
## 补充(2026-06-21
宝塔面板 HTTPS 多为自签证书;B 机 `curl` 下载签名 URL 时,当源机 `verify_ssl=false`(默认)自动加 `curl -k`,与 Nexus 连面板行为一致。
@@ -0,0 +1,29 @@
# 打包投递:curl 先落 /tmp 再 sudo mv
**日期**2026-06-21
## 摘要
`pull_transfer_package` / deliver 在目标机下载归档时,改为 `curl → /tmp/nexus-xfer-*``mv` 到目标路径(走 `exec_ssh_command_with_fallback` 提权),修复 `curl: (23) Failed writing body`(直写 `/www/...` 无权限)。
## 动机
打包投递时 Nexus 中心返回 200,但 B 机 `curl -o /www/...` 因 SSH 用户无写权限失败。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/application/services/server_file_transfer_service.py` | `_download_url_to_remote_path` |
| `server/api/sync_v2.py` | deliver 未捕获异常 → 502 |
| `tests/test_server_file_transfer.py` | tmp+mv 单测 |
## 验证
```bash
pytest tests/test_server_file_transfer.py::test_pull_downloads_via_tmp_then_mv -q
```
## 回滚
恢复 `curl -o dest` 直写并重启 API。
@@ -0,0 +1,20 @@
# 跨服务器传输:目标机 B 目录浏览
**日期**2026-06-21
## 摘要
「目标与方式」区域增加 B 服务器文件列表,可浏览目录;进入目录后自动同步下方目标路径(直传为目录,打包为目录下归档名)。
## 动机
仅文本框填路径不直观;需在目标机侧像源机一样点选目录确认落点。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``browseDest``destCurrentPath``syncDestPathFromBrowse`
- `frontend/src/pages/ServerTransferPage.vue` — 目标机文件表
## 验证
选 B 后出现文件列表;点目录进入并更新目标路径字段;`vite build` 通过。
@@ -0,0 +1,31 @@
# 跨服务器直传:目标机写入提权
**日期**2026-06-21
## 摘要
直传(relay)在目标机写入时改用 `remote_write_bytes`SFTP 失败自动 `sudo tee`),与文件上传一致;修复目标目录无写权限时 500 且前端仅显示「操作失败」。
## 动机
生产日志:`asyncssh.sftp.SFTPPermissionDenied``dst_sftp.open(..., "wb")`;宝塔子机常见 www 目录需 sudo 写入。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/application/services/server_file_transfer_service.py` | relay 写入/建目录提权 |
| `server/api/sync_v2.py` | relay 未捕获异常 → 502 + detail |
| `frontend/src/composables/useServerFileTransfer.ts` | `formatApiError` 展示后端 detail |
## 验证
```bash
pytest tests/test_server_file_transfer.py -q
```
传输页直传至 B 机 `target_path` 下目录;失败时应显示具体原因而非「操作失败」。
## 回滚
恢复裸 SFTP `dst_sftp.open` 并重启 API。
@@ -0,0 +1,31 @@
# 跨服务器传输:目标机须手动选择
**日期**2026-06-21
## 摘要
移除传输页在选定源服务器后自动选中列表第一台作为目标机(B)的行为;B 机默认留空,用户须在目标下拉框中手动选择。
## 动机
用户反馈 B 服务器不应自动选择,避免误传到错误子机。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `frontend/src/composables/useServerFileTransfer.ts` | `onSourceServerChange``applyRouteQuery` 去掉 `candidates[0]` |
## 行为
- 从文件页跳转(`?source=&path=&paths=`)仅预填 A 机与选中路径,B 机为空。
- 换 A 机时:若当前 B 与 A 相同则清空 B;否则保留用户已选手动 B。
- URL 显式带 `?dest=``?dest_server_id=` 时仍可预填 B(深链场景)。
## 验证
打开 `#/server-transfer?source=…`,确认目标服务器下拉为空,须手动选择后方可传输。
## 回滚
恢复 `candidates[0]` 自动赋值逻辑并重新构建前端。
@@ -0,0 +1,44 @@
# 跨服务器传输独立页面
**日期**2026-06-21
## 摘要
将「跨服务器文件直传 / 打包投递」从文件管理页内嵌对话框拆分为独立 SPA 页面 `#/server-transfer`,侧栏新增入口;文件页工具栏与右键改为跳转并预填源机与路径。
## 动机
- 传输流程涉及源机浏览、多选、目标机与模式选择,独立页比对话框更易操作与扩展。
- 与文件管理职责分离:文件页专注浏览编辑,传输页专注 A→B 编排。
## 涉及文件
| 路径 | 变更 |
|------|------|
| `frontend/src/pages/ServerTransferPage.vue` | 新建完整页(源浏览 + 目标配置 + 执行) |
| `frontend/src/composables/useServerFileTransfer.ts` | 新建页 composable;支持 query `source`/`path`/`paths` |
| `frontend/src/router/index.ts` | 路由 `/server-transfer` |
| `frontend/src/App.vue` | 侧栏「跨服务器传输」 |
| `frontend/src/composables/files/useFilesActions.ts` | 移除内嵌传输逻辑,改为 `goToServerTransfer` |
| `frontend/src/composables/files/useFilesPage.ts` | 导出 `goToServerTransfer` |
| `frontend/src/components/files/FilesToolbar.vue` | 按钮跳转独立页 |
| `frontend/src/components/files/FilesDialogs.vue` | 移除传输对话框 |
| `web/app/assets/ServerTransferPage-*` | Vite 构建产物 |
## 行为说明
- 文件页选中项后点「发送到其他服务器」→ `#/server-transfer?source=&path=&paths=`(路径 `|` 分隔)。
- 独立页可手动输入绝对路径、浏览源机目录、直传或打包投递;目标路径仍用 `localStorage``nexus:file-transfer-dest:{mode}:{serverId}`
- 后端 API 不变:`POST /sync/server-transfer``POST /sync/server-transfer/deliver`
## 迁移 / 重启
- 仅前端静态资源更新,无需 DB 迁移。
- 部署:重新 `vite build` 并同步 `web/app/`
## 验证
```bash
cd frontend && npx vite build
# 浏览器:侧栏「跨服务器传输」;文件页选中后跳转预填;直传/投递 smoke
```
@@ -0,0 +1,24 @@
# 跨服务器传输页:服务器选择与搜索修复
**日期**2026-06-21
## 摘要
修复 `#/server-transfer` 源/目标服务器下拉无法选择的问题,并对齐文件页:支持按名称、域名、路径、ID 搜索过滤。
## 动机
- `v-autocomplete` 未使用 `v-model`,选项 slot 误用 `item.raw` 导致 Vuetify 4 下点击无效。
- 2000+ 台机器无客户端搜索,无法快速定位服务器。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``filteredSourceServerList` / `filteredDestServerList`
- `frontend/src/pages/ServerTransferPage.vue` — 与文件页一致的选择器 UI
## 验证
```bash
cd frontend && npx vite build
# 打开 #/server-transfer:可搜索并选择源/目标服务器
```
@@ -0,0 +1,17 @@
# 跨服务器传输:源机 A 下方展示可点击网址
**日期**2026-06-21
## 摘要
选择源服务器 A 后,在服务器选择框下方显示站点域名链接(`resolveServerSiteUrl`),新标签页打开。
## 涉及文件
- `frontend/src/composables/useServerList.ts``ServerBrief` 增加 `description` / `extra_attrs`
- `frontend/src/composables/useServerFileTransfer.ts``sourceSiteUrl` / `sourceSiteHost`
- `frontend/src/pages/ServerTransferPage.vue` — 链接展示
## 说明
未 push Gitea;需上线时由用户指示再传输。
@@ -0,0 +1,28 @@
# 跨服务器传输:默认进入各机目标路径
**日期**2026-06-21
## 摘要
选源服务器 A 后浏览目录默认进入 A 的 `target_path`;选目标服务器 B 后「目标目录/归档路径」默认填入 B 的 `target_path`(切换 B 时不用旧 localStorage 覆盖)。
## 动机
与文件管理页一致:运维以各机业务根目录为起点,减少每次手动改路径。
## 涉及文件
- `frontend/src/composables/useServerFileTransfer.ts``serverTargetPath``applyDestPathForServer`
## 行为
| 操作 | 默认路径 |
|------|----------|
| 选 A | 左侧浏览 `A.target_path`(无则 `/www/wwwroot` |
| 从文件页带 `?path=` | 仍用 query 路径 |
| 选 B | 直传 → `B.target_path`;打包 → `B.target_path/xxx.tar.gz` |
| 仅切换直传/打包 | 仍可读该 B 机上次保存的路径 |
## 验证
`cd frontend && npx vite build`;页内选 A/B 观察路径字段。
@@ -0,0 +1,40 @@
# 跨服务器传输:传输后 www 权限
**日期**2026-06-21
## 摘要
跨服务器直传(relay)与打包投递(deliver/pull)完成后,在目标机上自动执行 `chown -R www:www``NEXUS_RSYNC_PUSH_CHMOD` 对应 chmod,与 rsync 推送、文件上传后权限策略一致。
## 动机
用户要求「传输过去的文件都要是 www 权限」;此前 SFTP 中继与 curl 解压只写文件,不调整属主,导致宝塔站点无法读文件。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/utils/files_upload_permissions.py` | 新增 `apply_transfer_permissions``parse_upload_dir_mode` |
| `server/application/services/server_file_transfer_service.py` | relay / pull 完成后调用权限修复 |
| `tests/test_files_upload_permissions.py` | 递归 chown/chmod 单测 |
| `tests/test_server_file_transfer.py` | relay mock `_apply_dest_permissions` |
| `deploy/rsync-local-to-server.sh` | 中心机无 `www` 用户时自动用部署目录属主 |
| `deploy/deploy-production.sh` | 同上,前端 tar 解压 chown |
## 配置
- `NEXUS_RSYNC_PUSH_CHOWN`(默认 `www:www`
- `NEXUS_RSYNC_PUSH_CHMOD`(默认 `D755,F755`
- 中心机部署属主:`NEXUS_DEPLOY_CHOWN` 可覆盖;未设置且无 `www` 用户时用 `stat` 检测
## 验证
```bash
pytest tests/test_files_upload_permissions.py tests/test_server_file_transfer.py -q
```
生产传输页直传/投递后,目标路径 `ls -la` 应为 `www:www`
## 回滚
还原上述 Python 文件并重启 API;部署脚本属主检测为向后兼容,可保留。
@@ -0,0 +1,29 @@
# 主服务器列表增加宝塔一键登录
**日期**2026-06-21
## 变更摘要
在主服务器列表(`#/servers`)每行操作列增加「宝塔」按钮,调用 `POST /api/btpanel/servers/{id}/login-url` 在新标签页打开宝塔临时登录链接(API 优先,失败则 SSH 回退)。
## 动机
运维在浏览全量服务器时无需先进入宝塔子模块即可一键登录面板。
## 涉及文件
- `frontend/src/components/servers/ServerTableRowActions.vue` — 新增「宝塔」按钮
- `frontend/src/composables/btpanel/useBtPanelLogin.ts` — 登录逻辑复用
- `frontend/src/pages/ServersPage.vue` — 主表与未设路径表接线
- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 透传 loading / emit
- `frontend/src/constants/serverTableHeaders.ts` — 操作列宽 320→380
## 迁移 / 重启
无。仅前端构建部署。
## 验证方式
1. `cd frontend && npx vite build`
2. 打开 `#/servers`,任一行点击「宝塔」,应新标签打开宝塔登录页
3. 未配置 API 的机器仍可走 SSH 回退(需 SSH 可达且已装宝塔)
@@ -0,0 +1,44 @@
# 服务器列表操作列与批量栏调整
**日期**2026-06-21
## 变更摘要
- 行内宝塔按钮文案改为「一键登录」
- 移除行内「监测」「站点」按钮(站点链接仍在名称/域名列可点)
- 「检测路径」「诊断」从每行操作移至顶部批量操作栏(需勾选服务器)
- 批量「检测路径」文案统一;「诊断」须单选 1 台
## 动机
行操作过多拥挤;路径检测与 Agent 诊断属批量运维动作,与「健康检查」同类。
## 涉及文件
- `frontend/src/components/servers/ServerTableRowActions.vue`
- `frontend/src/components/servers/ServerBatchActionBar.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/servers/ServerUnsetPathPanel.vue`
- `frontend/src/constants/serverTableHeaders.ts`
## 迁移 / 重启
仅前端构建部署。
## 验证
1. `#/servers` 行内见分组 outlined 按钮(终端/登录、文件/编辑、删除图标)
2. 勾选多台 → 批量栏分组展示运维 / Agent / 其他
3. 单选 1 台 → 批量栏「诊断」可用
## 2026-06-21 UI 迭代
- 行操作:`v-btn-group` outlined + 图标;宝塔入口显示「登录」+ tooltip
- 批量栏:浅底描边卡片,按运维/Agent/其他分组,tonal 按钮带图标
## 2026-06-21 可读性修正
- 行按钮改 `small` + `tonal` + 字重 600,恢复「一键登录」全文
- 批量栏改 `flat` 实色按钮、分组标题加深,底卡用 surface 实底
- 行操作移除「删除」;删除请用批量栏「批量删除」
- 批量栏「已选 N 台」增加「选择」分组标题,与其他分区对齐
@@ -0,0 +1,29 @@
# Changelog — SSH 预设凭据解析修复(2026-06-21)
**日期**2026-06-21
## 摘要
修复仅设置 `preset_id`、未复制 `password` 字段时 SSH/宝塔批量引导全部失败(`ssh_command_failed`)。
## 动机
任务 #113 等 10/10 失败:服务器关联密码预设「商城默认密码」,但 `asyncssh_pool` 只读行内 `password`,未解析预设。
## 涉及文件
- `server/infrastructure/ssh/resolve_server_auth.py`(新)
- `server/infrastructure/ssh/asyncssh_pool.py``acquire` 前物化凭据
- `server/infrastructure/btpanel/ssh_bootstrap.py``ssh_no_credentials` 错误码
- `tests/test_resolve_server_auth.py`
## 迁移 / 重启
- 需重启 API 容器
## 验证
```bash
.venv/bin/pytest tests/test_resolve_server_auth.py tests/test_btpanel_*.py -q
# 重跑宝塔批量 SSH 获取 API
```
@@ -0,0 +1,30 @@
# 宝塔一键登录临时时效改为 24 小时
**日期**2026-06-22
## 摘要
一键登录生成临时 `tmp_token` 时,`expire_time` 设为当前时间 + **86400 秒**24 小时);SSH 回退不再调用 `tools.py get_temp_login_ipv4`(固定 3 小时),改为远程执行同等逻辑并传入 TTL。
## 动机
用户希望登录后面板会话与临时链接上限为 24 小时;宝塔默认 3 小时,且 `tmp_login_expire` 为生成链接时的绝对时间。
## 涉及文件
- `server/infrastructure/btpanel/constants.py``BT_TEMP_LOGIN_TTL_SECONDS = 86400`
- `server/application/services/btpanel_service.py` — API `set_temp_login``expire_time`,解析 `token` 拼 URL
- `server/infrastructure/btpanel/ssh_login.py``build_ssh_temp_login_command`
- `tests/test_btpanel_temp_login_ttl.py`
## 迁移 / 重启
仅 API 变更;部署后端镜像即可,前端无改动。
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_temp_login_ttl.py tests/test_btpanel_ssh_login.py -q
```
生产:`POST /api/btpanel/servers/{id}/login-url` 后登录,约 24 小时内不应提示「临时登录已过期」(仍受面板 `session_timeout.pl` 约束)。
@@ -0,0 +1,32 @@
# Changelog — 宝塔改端口后一键登录失败修复
**日期**2026-06-23
## 摘要
修复子机宝塔面板改端口后,Nexus「一键登录」返回「宝塔操作失败」的问题。旧 `base_url` 连接失败时现会 SSH 同步新端口并重试 API,仍失败则走 SSH 临时登录。
## 动机
用户改宝塔面板端口后,库内 `extra_attrs.bt_panel.base_url` 仍为旧端口。`httpx.ConnectError` 未被捕获,未触发既有 SSH 回退,接口直接 500。
生产日志:`POST /api/btpanel/servers/404/login-url``httpx.ConnectError: All connection attempts failed`
## 涉及文件
- `server/infrastructure/btpanel/client.py` — 网络错误包装为 `BtPanelApiError`
- `server/application/services/btpanel_service.py``_try_login_url_via_api`:失败时 `_repair_ip_whitelist``port.pl` 更新地址后重试,再 SSH 回退
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_client.py`
## 迁移 / 重启
- 无 DB 迁移;部署后端并重启 API 容器。
## 验证
```bash
pytest tests/test_btpanel_login_url.py tests/test_btpanel_client.py -q
```
改端口子机:服务器列表 → 一键登录宝塔 → 应打开新端口临时登录页。
@@ -0,0 +1,43 @@
# 检测路径改为 nginx 网站目录
**日期**2026-06-23
## 变更摘要
批量「检测路径」由搜索 `workerman.bat` 改为读取 nginx 虚拟主机配置中的 `root` 网站目录,并写入 `target_path`
## 动机
站点目录应以 nginx 配置为准;`workerman.bat` 并非通用标志,大量服务器检测失败。
## 检测逻辑
1. 取服务器 `domain`(域名或 IP
2. SSH 远程执行:
- 优先 `/www/server/panel/vhost/nginx/{domain}.conf`(宝塔)
- 再扫描宝塔 vhost 中 `server_name` 匹配项
- 最后尝试 `/etc/nginx/sites-enabled``/etc/nginx/conf.d`
3. 解析首个 `root` 指令,校验为合法绝对路径后写入 DB
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/utils/nginx_path_detect.py` | 新增:构建远程检测命令 |
| `server/application/server_batch_common.py` | `detect_and_save_target_path` 改用 nginx |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
| `docs/project/nexus-functional-development-guide.md` | 功能说明 |
## 迁移 / 重启
无需 DB 迁移;后端热更新或重启 API 即可。前端需 `vite build` 后部署。
## 验证
```bash
.venv/bin/pytest tests/test_nginx_path_detect.py tests/test_server_onboarding.py -q
```
服务器页勾选多台 →「检测路径」→ 确认说明含 nginx `root` → 任务结果 `target_path → /www/wwwroot/...`
@@ -0,0 +1,36 @@
# Changelog — 订阅 IP 自动刷新 Primary 锁与 last_refresh 持久化
**日期**2026-06-23
## 摘要
修复单 worker 生产环境下 Redis 主锁竞态导致 `ip_allowlist_refresh_loop` 永不启动、进程内存订阅 IP 与 DB/实时订阅脱节的问题;将「上次刷新」时间写入 `login_subscription_last_refresh` 并广播。
## 动机
生产排查发现:
- `GET /api/settings/ip-allowlist` 返回的 `subscription_ips` 与 DB、实时拉取订阅结果不一致(内存陈旧)。
- `last_refresh` 长期为空;`nexus:primary_worker` 锁未持有,2 小时后台刷新未运行。
- 容器重启时若 Redis 仍残留旧 PID 锁,单 worker 启动一次抢锁失败后不再重试,后台任务永久禁用。
## 涉及文件
- `server/main.py` — 单 worker 跳过 Redis 选举;多 worker 启动重试抢锁
- `server/background/ip_allowlist_refresh.py` — 持久化 `login_subscription_last_refresh`
- `server/config.py` — 新 settings 键映射
- `server/api/settings.py` — 敏感键列表
- `tests/test_ip_allowlist_sync.py`
## 迁移 / 重启
- 无 DB 迁移(新 key 首次刷新时自动写入)。
- 需重启 API 容器使 primary 修复与刷新循环生效。
## 验证
```bash
pytest tests/test_ip_allowlist_sync.py tests/test_login_access.py -q
```
生产:重启后约 5s 内应完成首次订阅刷新;设置页「上次刷新」有值且节点 IP 与订阅一致。
@@ -0,0 +1,42 @@
# IP-only 服务器自动补全站点域名
**日期**: 2026-06-24
**类型**: feat(backend)
## 背景
大量子机 SSH 地址登记为 IP,服务器列表「地址」列缺少可点击站点链接。需每日自动 SSH 探测 nginx 站点名并写入元数据。
## 改动
- 新增 `server/utils/site_host.py`:判定 IP-only 与解析已有站点 host(对齐前端 `serverSiteUrl.ts`)。
- 新增 `server/utils/nginx_domain_detect.py`:远程扫描 Baota/nginx vhost 提取域名。
- 新增 batch op `detect-domain`SSH 探测并写 `extra_attrs.site_url`(不改 SSH `domain` 字段)。
- 新增定时任务 `ip_domain_detect_loop`:北京时间 **23:30** 每日触发(`IP_DOMAIN_DETECT_CRON=30 23 * * *`)。
- 配置:`IP_DOMAIN_DETECT_ENABLED`(默认 true)。
## 涉及文件
- `server/application/services/ip_domain_detect_schedule.py`
- `server/background/ip_domain_detect_loop.py`
- `server/application/server_batch_common.py`
- `server/application/services/server_batch_service.py`
- `server/config.py`, `server/main.py`
- `docs/design/specs/2026-06-24-ip-domain-detect-schedule-design.md`
- `docs/design/plans/2026-06-24-ip-domain-detect-schedule.md`
## 迁移 / 重启
- 无 DB 迁移;部署后重启 API 容器使 background loop 生效。
## 验证
```bash
pytest tests/test_site_host.py tests/test_nginx_domain_detect.py tests/test_ip_domain_detect_schedule.py -q
```
- 生产:23:30 后审计日志应出现 `batch_detect_domain``#/servers` 中 IP-only 机器地址列下方出现域名链接。
## 回滚
`.env` 设置 `IP_DOMAIN_DETECT_ENABLED=false` 并重启。
@@ -0,0 +1,77 @@
# 2026-06-24 — 未设路径区块与服务器列表对齐
## 变更摘要
调整 `#/servers` 页面「未设置路径服务器」与「服务器列表」的布局顺序与表格列宽,使两表视觉对齐并符合设计文档区块顺序。
## 动机
用户反馈未设路径区块与主列表未对齐:区块顺序与设计(主列表 → 未设路径 → 连接失败)不一致,且两张独立 `v-data-table` 列宽各自计算导致列边界错位。
## 涉及文件
- `frontend/src/pages/ServersPage.vue` — 将 `ServerUnsetPathPanel` 移至主列表卡片之后;提示文案「上方」改「下方」
- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 标题栏样式与主表一致;统一 `server-data-table` 类;修复 install/upgrade emit 类型
- `frontend/src/constants/serverTableHeaders.ts` — 补全各列 `minWidth`
- `frontend/src/styles/server-data-table.css` — 共享 `table-layout: fixed` 与展开列隐藏
- `frontend/src/main.ts` — 全局引入表格样式
## 迁移 / 重启
无。前端 rebuild 后生效。
## 验证
1. 打开 `#/servers`,确认顺序:分类筛选 → **服务器列表****未设置路径服务器** → 连接失败列表
2. 对比两表列头(状态/名称/地址/分类/目标路径/Agent/心跳/操作)左右边界对齐
3. 分类筛选后,主表为空且存在未设路径机器时,提示「已在下方区块展示」,点击「定位」滚动至未设路径卡片
---
# 2026-06-24 — 未设路径区块工具栏与主列表对齐
## 变更摘要
「未设置路径服务器」卡片标题栏右侧增加与「服务器列表」相同的搜索、添加、凭据、检测路径、列表/分类切换;保留刷新按钮。
## 动机
用户要求未设路径区块具备与主列表一致的操作入口,避免只能在上方主表标题栏操作。
## 涉及文件
- `frontend/src/components/servers/ServerListToolbarActions.vue` — 新建共享工具栏
- `frontend/src/components/servers/ServerUnsetPathPanel.vue``#toolbar` 插槽
- `frontend/src/pages/ServersPage.vue` — 主表与未设路径表共用组件
## 迁移 / 重启
无。前端 rebuild 后生效。
## 验证
1. `#/servers` 未设路径卡片标题栏可见搜索框及添加/快速添加/批量添加/凭据/检测路径/列表·分类切换/刷新
2. 在未设路径区搜索、添加服务器,主表与未设路径表同步刷新
3. 未设路径区「检测路径」按本区勾选数启用/禁用
---
# 2026-06-24 — 未设路径工具栏内嵌修复
## 变更摘要
将工具栏从 slot 改为 `ServerUnsetPathPanel` 内直接渲染;标题栏允许换行,避免 `v-card-title` 裁切右侧按钮。
## 动机
用户反馈未设路径区块仍看不到搜索/快速添加等工具栏(slot + 单行标题溢出)。
## 涉及文件
- `frontend/src/components/servers/ServerListToolbarActions.vue` — 单根容器 + flex-wrap
- `frontend/src/components/servers/ServerUnsetPathPanel.vue` — 内嵌工具栏
- `frontend/src/pages/ServersPage.vue` — v-model 绑定搜索与视图模式
## 验证
`npx vite build` 通过后,刷新 `#/servers` 向下滚动至「未设置路径服务器」,标题行右侧应可见完整工具栏。
@@ -0,0 +1,29 @@
# 宝塔 bootstrap 无变更时跳过 bt reload
**日期**2026-06-29
## 摘要
SSH bootstrap 对接宝塔 API 时,若 `api.json` 无需变更(API 已开、token 已有、中心 IP 已在白名单),**不再写盘、不执行 `/etc/init.d/bt reload`**,避免每 5 分钟重试或重复一键登录时踢掉面板已有会话。
## 动机
商城新机 `43.138.205.6` 在 Nexus bootstrap 时 `bt reload` 导致 15:32 面板全员掉线;对已配置机器重复 bootstrap 不应 reload。
## 涉及文件
- `server/infrastructure/btpanel/ssh_bootstrap.py``if actions:` 才写 `api.json` + reloadstdout 增加 `reloaded`
- `tests/test_btpanel_ssh_bootstrap.py` — 断言远程脚本条件 reload
- `docs/design/specs/2026-06-20-btpanel-module-design.md` — 设计说明同步
## 迁移 / 重启
仅后端;`deploy-production.sh` rsync + Docker `upgrade --skip-git`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py -q
```
生产:对已 bootstrap 完成的子机再触发对接,syslog 不应再出现 `bt reload``actions` 为空时)。
@@ -0,0 +1,38 @@
# 每日离线巡检:发报前 SSH 复检 + 备注优先显示
**日期**2026-06-30
## 变更摘要
定时「每日离线巡检」Telegram 日报在统计与推送前,先对当前离线子机批量执行 SSH 健康检查并刷新 Redis 心跳,再二次扫描得到最终在线/离线数;日报正文增加「巡检前 SSH 复检」一行。离线列表与离线告警的显示名与批量任务一致:**备注 description → name → domain**。
## 动机
Agent 心跳滞后或短暂网络抖动会导致误报离线;日报发送前应主动 SSH 探测确认。离线名单在 Telegram 中应优先展示运维备注,便于识别机器。
## 涉及文件
- `server/application/server_connectivity.py``health_check_server_ids()``scan_monitored_connectivity``batch_server_display_name`
- `server/application/services/offline_daily_report_schedule.py` — 日报前 preflight 健康检查
- `server/config.py``OFFLINE_DAILY_REPORT_HEALTH_CHECK`(默认 true
- `server/infrastructure/telegram/__init__.py` — 日报增加 preflight 统计行
- `server/background/server_offline_monitor.py` — 离线告警显示名备注优先
- `tests/test_offline_daily_report_schedule.py` — preflight 流程单测
## 配置
```env
OFFLINE_DAILY_REPORT_HEALTH_CHECK=true # false 可跳过复检
```
## 迁移 / 重启
无 DB 迁移;部署后 Docker 重建 API 容器即可,background loop 随进程启动。
## 验证方式
```bash
.venv/bin/pytest tests/test_offline_daily_report_schedule.py tests/test_batch_server_display_name.py -q
```
生产:手动触发 `run_scheduled_offline_daily_report(force=True)` 或等到 11:50 北京时间;Telegram 应含复检行且离线名单显示备注。
@@ -0,0 +1,38 @@
# Agent 双模式监测:四槽 5s 出站推送,跳过 SSH
**日期**2026-07-01
## 变更摘要
单 Agent 双模式落地:
1. **常态**60s 智能心跳(变化 <10% 可 skip**30s keepalive** 拉取 `watch_active`
2. **监测**:四槽 Pin 且开启实时监测 → 中心响应 `watch_active` → Agent **5s 全量上报**(含 `load_avg``watch_mode`
3. **探针**`watch_probe_runner` 在 Agent watch 心跳新鲜时 **跳过 SSH**;进程表仍偶发 SSH 拉取;无 Agent/旧版仍 SSH 兜底
## 动机
四槽 5s SSH 探针对子机形成长期连接与频繁握手,易触发风控;子机已有 nexus-agent 出站通道,应按需切高频而非双 Agent。
## 涉及文件
- `web/agent/agent.py` — v2.1.0 双模式心跳
- `web/agent/config.example.json``heartbeat_keepalive_sec` / `watch_interval_sec`
- `server/api/agent.py` — 心跳响应 `watch_active` / `watch_interval_sec`
- `server/background/watch_probe_runner.py` — Agent 路径优先
- `server/utils/watch_metrics.py``WATCH_AGENT_FRESH_SEC`、usable 判定
- `tests/test_agent_watch_mode.py`
## 迁移
- 中心:部署 API 镜像
- 子机:**仅 Pin 监测中的机器**需升级 Agent 至 2.1.0(面板推送或 batch push
- 未升级 Agent:行为与改前相同(SSH 5s)
## 验证
```bash
.venv/bin/pytest tests/test_agent_watch_mode.py tests/test_watch_metrics.py -q
```
Pin 一台已装 Agent 的子机 → 开实时监测 → 探针记录 `source=agent`;移除槽后 Agent 日志恢复 60s。
@@ -0,0 +1,54 @@
# 补录门禁:宝塔 skip-reload · Terminal 快速添加 · 快捷命令排序
**日期**2026-07-01
## 变更摘要
补录 2026-06-30 / 07-01 已上线但未经完整八道门控的批次:
1. **宝塔 bootstrap**`api.json` 无变更时不 `bt reload`,避免踢面板会话
2. **Terminal**:工具栏「快速添加(IP)」与 Servers 对齐;成功后自动 `newSession` / 跳转 Terminal
3. **Settings 快捷命令**`PUT /quick-commands/reorder` 路由置于 `/{id}` 之前,修复上下箭头无效
## 动机
- 6/30 生产部署跳过 `pre_deploy_check`(无当日 changelog/audit、本机无 :8600、Gate 8 无 cursor-agent
- 用户反馈 Settings 上下箭头点击无效应从根因修复并补门禁
## 涉及文件
**后端**
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/api/terminal.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_terminal_quick_commands.py`
**前端**
- `frontend/src/composables/servers/useServerQuickAdd.ts`
- `frontend/src/components/servers/ServerQuickAddDialogs.vue`
- `frontend/src/utils/openServerTerminal.ts`
- `frontend/src/components/terminal/TerminalTabBar.vue`
- `frontend/src/pages/TerminalPage.vue`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/components/TerminalQuickCommandsSettings.vue`
**门禁**
- `deploy/pre_deploy_check.sh` — Gate 3 本机 :8600 不可达时回退 pytest 专项
- `docs/changelog/2026-06-29-btpanel-bootstrap-skip-reload.md`(6/29 变更说明,一并纳入本批验证)
## 迁移 / 重启
- 宝塔 skip-reloadNexus 中心 `deploy-production.sh` 重建 API 镜像
- Terminal / 快捷命令:前端 `vite build` + 同步 `web/app`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py tests/test_terminal_quick_commands.py -q
bash deploy/pre_deploy_check.sh
```
生产:Terminal 快速添加 → 自动开 SSH 标签;Settings 快捷命令上下箭头可调序。
@@ -0,0 +1,35 @@
# Agent 2.1.1:监测模式 CPU 采样优化
**日期**2026-07-02
## 变更摘要
修复四槽实时监测下 CPU 曲线长期显示 **0%** 的问题:
1. 新增 `web/agent/cpu_metrics.py`:监测模式用 `interval=None` 测约 **5s 窗口**(与心跳间隔对齐);首次进入 watch 用 **1s bootstrap**
2. `agent.py` 升至 **v2.1.1**`_build_system_info` 改用 `sample_watch_cpu_bundle`(含 `per_cpu_pct`
3. 升级/安装脚本同步下载 `cpu_metrics.py`
## 动机
v2.1.0 监测模式用 `psutil.cpu_percent(interval=0.3)`,低负载机器常四舍五入为 0;内存/负载正常,属采样窗口过短而非探针故障。
## 涉及文件
- `web/agent/cpu_metrics.py`(新建)
- `web/agent/agent.py` — v2.1.1
- `server/application/server_batch_common.py` — curl 下载 cpu_metrics
- `tests/test_agent_cpu_metrics.py`(新建)
## 迁移
- 中心:部署后 `/agent/cpu_metrics.py` 可访问
- 子机:**Pin 且开实时监测**的机器点槽内「升级 Agent」至 2.1.1
## 验证
```bash
.venv/bin/pytest tests/test_agent_cpu_metrics.py tests/test_agent_watch_mode.py -q
```
升级后探针 `cpu_pct` 在低负载下应出现非零小幅波动,不再长期贴 0。
@@ -0,0 +1,29 @@
# 统计卡:新增「未设路径·离线」
**日期**2026-07-02
## 变更摘要
仪表盘 / 服务器页顶部统计由 5 卡扩为 6 卡,新增 **未设路径·离线**
- 口径:`unset_path_offline = 全库 Agent 离线 主列表离线`
- 使 **在线 + 离线 + 未设路径·离线 ≈ 服务器总数**(消除原先 390+8≠428 的困惑)
- 点击跳转 `/servers?unset_path=1&status=offline`,定位未设路径区块并筛离线
## 动机
在线卡统计全库 Agent 在线(含未设路径),离线卡仅主列表离线;约 30 台「未设路径且离线」无处展示,用户误以为统计错误。
## 涉及文件
- `server/api/servers.py``unset_path_offline` 字段
- `frontend/src/composables/useServerStatsCards.ts`
- `frontend/src/pages/ServersPage.vue`
- `frontend/src/pages/DashboardPage.vue`
- `tests/integration/test_servers_dashboard.py`
## 验证
```bash
.venv/bin/pytest tests/integration/test_servers_dashboard.py -q
```
@@ -0,0 +1,29 @@
# Terminal 工具栏一键登录宝塔
**日期**2026-07-03
## 变更摘要
在 WebSSH 终端页,当已打开服务器会话时,于工具栏「字号」左侧增加 **宝塔登录** 按钮;复用服务器列表同一套 `useBtPanelLogin` 逻辑,对当前激活标签的 `serverId` 生成并打开宝塔面板登录链接。
## 动机
运维在终端排障时常需切到宝塔面板,此前只能回服务器列表点「一键登录」;在终端内直接登录可减少跳转。
## 涉及文件
**前端**
- `frontend/src/components/terminal/TerminalToolbar.vue` — 桌面按钮 + 移动端齿轮菜单项
- `frontend/src/pages/TerminalPage.vue` — 接入 `useBtPanelLogin`、loading 与点击处理
## 迁移 / 重启
- 仅前端静态资源:`bash deploy/deploy-frontend.sh`Docker 生产走 `nexus-1panel.sh upgrade` 重建镜像内前端)
## 验证
```bash
cd frontend && npm run build-only
# 打开 #/terminal,连接任意服务器后工具栏应显示「宝塔登录」
```
@@ -0,0 +1,27 @@
# 告警连续超阈门控(C2
**日期**2026-07-04
## 变更
CPU/内存/磁盘告警需 **连续 N 次**(默认 **3**)心跳超阈才触发 `broadcast_alert`;单次尖峰不再入库/Telegram。
| 项 | 说明 |
|----|------|
| 配置 | `alert_streak_required`(设置页「连续超阈次数」,默认 3) |
| 常态心跳 60s | 约 **3 分钟**持续超阈才告警 |
| 监测槽 5s | 约 **15 秒**持续超阈才告警 |
| 设为 1 | 恢复旧行为(单次超阈即告警) |
| Redis | `alert_streak:{server_id}:{metric}`,回落正常后清零 |
## 文件
- `server/utils/alert_metrics.py` — 阈值检测共用
- `server/utils/alert_streak.py` — 连续计数 + 告警/recovery 门控
- `server/api/agent.py` — 心跳接入
- `server/utils/watch_alerts.py` — 监测探针接入
- `tests/test_alert_streak.py`
## 部署
中心机 `nx update``deploy-production.sh`**无需**子机 Agent 升级。
@@ -0,0 +1,30 @@
# 检测路径改为搜索 crmeb 目录
**日期**2026-07-05
## 变更摘要
批量「检测路径」由在 `/www/wwwroot` 下搜索 `workerman.bat` 改为搜索名为 `crmeb` 的目录,并将其完整路径写入 `target_path`
## 检测逻辑
```bash
find /www/wwwroot -iname 'crmeb' -type d -print -quit 2>/dev/null
```
示例:`/www/wwwroot/shop.example.com/crmeb``target_path` = `/www/wwwroot/shop.example.com/crmeb`
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/application/server_batch_common.py` | `detect_and_save_target_path` |
| `frontend/src/pages/ServersPage.vue` | 确认对话框文案 |
| `scripts/batch_detect_target_path.py` | 脚本说明 |
| `tests/test_nginx_path_detect.py` | 单元测试 |
## 验证
```bash
.venv/bin/pytest tests/test_nginx_path_detect.py -q
```
@@ -0,0 +1,33 @@
# 宝塔 bootstrap:真安装检测 + config 目录自愈
**日期**2026-07-08
## 摘要
SSH bootstrap 写 `api.json` 前校验 `class/common.py` 存在(空 `panel` 目录不再误判为已装宝塔);写盘前 `makedirs` config 目录,避免 `FileNotFoundError` 被归类为模糊的 `ssh_command_failed`
## 动机
新机 **山西鑫途汇**`42.193.230.144`)入库时仅有空 `/www/server/panel/`,一键登录反复 `ssh_command_failed`;同公司 M 机宝塔正常。根因是远程脚本只检查 `panel` 目录存在,写 `config/api.json` 时父目录缺失崩溃。
## 涉及文件
- `server/infrastructure/btpanel/ssh_bootstrap.py``common.py` 检测、`os.makedirs` config
- `tests/test_btpanel_ssh_bootstrap.py` — 远程脚本断言
## 迁移 / 重启
仅后端;`rsync-local-to-server.sh` + Docker `upgrade --skip-git`
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_bootstrap.py -q
```
生产:对 `bt_installed=false` 的机器一键登录应返回 `bt_not_installed`installing 状态),不再 `ssh_command_failed`
## 2026-07-08 热修(回归)
- **根因**2026-06-21「一键登录自动 bootstrap」在 `bootstrap` 失败时直接 `raise`,不再走 SSH `tools.py` 临时登录;新机 API 未就绪时整链中断。
- **修复**`create_login_url` 对非凭据类 bootstrap 错误(如 `ssh_command_failed`)记录 warning 后 **继续 SSH 回退**;仅 `ssh_auth_failed` / `ssh_no_credentials` 等硬失败仍中断。
@@ -0,0 +1,28 @@
# 宝塔未安装时一键登录友好提示
**日期**2026-07-09
## 摘要
未安装宝塔的子机点击「宝塔一键登录」时,不再抛出 Python `ModuleNotFoundError: public` 堆栈,改为明确中文:`该机未检测到宝塔面板…`
## 动机
运维误对无宝塔机器点登录,SSH 回退脚本 `import public` 失败,前端只显示晦涩 Traceback。
## 涉及文件
| 路径 | 说明 |
|------|------|
| `server/infrastructure/btpanel/ssh_login.py` | `BtPanelNotInstalledError``humanize_ssh_login_error`、SSH 前检测 `common.py` |
| `server/application/services/btpanel_service.py` | bootstrap `bt_not_installed` 不再回退 SSH |
| `server/api/btpanel.py` | 400 + 友好 `detail` |
| `tests/test_btpanel_ssh_login.py` | 未安装 / public 模块报错映射 |
## 验证
```bash
.venv/bin/pytest tests/test_btpanel_ssh_login.py tests/test_btpanel_temp_login_ttl.py -q
```
对未装宝塔机器 `POST /api/btpanel/servers/{id}/login-url` → HTTP 400,detail 含「未检测到宝塔面板」。
@@ -0,0 +1,33 @@
# 跨服务器文件传输 — 实施计划
## 涉及文件
| 文件 | 变更 |
|------|------|
| `server/infrastructure/redis/transfer_package_store.py` | 新建:打包任务 Redis |
| `server/application/services/server_file_transfer_service.py` | 新建:直传 / 打包 / 拉取 / 流 |
| `server/api/schemas.py` | 请求模型 |
| `server/api/sync_v2.py` | 四个端点 |
| `server/api/auth_jwt.py` | 下载端点 JWT 豁免 |
| `frontend/src/composables/files/useFilesActions.ts` | 对话框逻辑 |
| `frontend/src/components/files/FilesDialogs.vue` | UI |
| `frontend/src/components/files/FilesList.vue` | 右键菜单 |
| `tests/test_server_file_transfer.py` | 单元测试 |
| `docs/changelog/2026-06-21-server-file-transfer.md` | 变更记录 |
## 步骤
1. Redis store + serviceSFTP 递归中继、远程 tar、目标 curl)
2. API + 审计
3. 前端对话框(模式切换、目标服务器、路径、打包后展示链接与一键 pull)
4. pytest + `scripts/local_verify.sh` 相关用例
## 回滚
删除新端点与服务文件;Redis key `transfer:pkg:*` 自然过期。无需 DB 迁移。
## 测试要点
- 校验:同源目标、空 sources、路径非法
- token 过期 / 不存在 → 404
- mock SFTP 的 relay 字节计数(可选)
@@ -0,0 +1,37 @@
# IP 域名自动探测 — 实施说明
**日期**: 2026-06-24
## 涉及文件
| 文件 | 动作 |
|------|------|
| `server/utils/site_host.py` | 新增:IP 判定与站点 host 解析 |
| `server/utils/nginx_domain_detect.py` | 新增:远程 nginx 域名探测命令 |
| `server/application/server_batch_common.py` | 扩展:探测并写 site_url |
| `server/application/services/ip_domain_detect_schedule.py` | 新增 |
| `server/background/ip_domain_detect_loop.py` | 新增 |
| `server/application/services/server_batch_service.py` | 新增 op `detect-domain` |
| `server/config.py` | 新增配置项 |
| `server/main.py` | 注册 loop |
| `tests/test_*.py` | 单元测试 |
| `docs/changelog/2026-06-24-ip-domain-detect-schedule.md` | changelog |
## 实现步骤
1. `site_host.py` 镜像前端 `serverSiteUrl.ts` 逻辑。
2. `build_nginx_domain_detect_command` 扫描 Baota vhost 文件名 + server_name。
3. `detect_and_save_site_url` SSH 执行并更新 `extra_attrs`
4. schedule 列出候选 → `start_batch_job(op="detect-domain")`
5. main 注册 loopconfig 默认 23:30。
## 测试要点
- cron 23:30 北京时区匹配
- `list_ip_only_without_site_server_ids` 过滤逻辑
- nginx 命令含 quote、跳过 IP basename
- schedule 幂等(already_ran_today
## 回滚
- `.env``IP_DOMAIN_DETECT_ENABLED=false` 并重启 API;或删除新增 loop 注册。
@@ -44,7 +44,7 @@
1. 无面板目录 → `bt_not_installed`(状态 `installing`5min 重试)
2. 读/写 `api.json`:保留 token、开启 API、追加中心 IP 到 `limit_addr`
3. 原子写 + `bt reload`stdout JSON → 加密写入 `api_key_enc` + `base_url`
3. 仅当 `api.json` 有变更(新 token / 开 API / 追加白名单)时原子写 + `bt reload`;已配置则跳过 reload,避免踢面板会话stdout JSON → 加密写入 `api_key_enc` + `base_url`
### 状态机(`extra_attrs.bt_panel`
@@ -0,0 +1,50 @@
# 跨服务器文件传输设计
## 背景与目标
文件管理器当前剪贴板仅支持**同一台服务器**内复制/移动。运维需要在 A 服务器选定文件或目录后,传到 B 服务器,无需经本地下载再上传。
对标宝塔「发送到服务器 / 打包下载链接」能力,提供两种模式:
| 模式 | 适用 | 数据路径 |
|------|------|----------|
| **直传 relay** | 单文件、多文件(含目录,递归 SFTP) | A → Nexus 中心 SFTP 中继 → B |
| **打包 package** | 大目录、批量归档 | A 上 tar.gz → Nexus 签名下载 URL → B `curl` 拉取(可解压) |
## 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. 中心 SFTP 中继 | 不依赖 A/B 互通;复用现有 SSH 凭据 | 带宽经中心;超大文件受 500MB/文件限制 |
| B. A 上 scp 到 B | 不经中心 | 需在 A 存 B 凭据;子机间常不通 |
| C. 仅打包 + wget | 适合目录;B 直连 API | 需临时公开下载端点(签名 token) |
**选定**:A(直传)+ C(打包拉取)组合;拒绝 B。
## 接口
前缀 `/api/sync``sync_v2.py`)。
| 方法 | 路径 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/server-transfer` | JWT | 直传 |
| POST | `/server-transfer/deliver` | JWT | 一键打包 + B 拉取 + 可选解压 |
| POST | `/server-transfer/package` | JWT | 仅打包(高级) |
| POST | `/server-transfer/pull` | JWT | 仅拉取(高级) |
| GET | `/transfer-download/{token}` | **token**(无 JWT | 流式下载归档 |
## 安全
- `source_server_id != dest_server_id`
- 路径校验复用 `assert_clipboard_transfer_safe` / `coerce_remote_abs_path`
- 下载 token 存 RedisTTL 1h;路径仅 token 可查
- `GET /transfer-download/` 加入 JWT 白名单前缀;token 即凭证
- 单文件直传上限 500MB(与现有下载一致)
- 全操作写 `audit_logs`
## 验收标准
- [ ] 文件页选中 1+ 项 →「发送到其他服务器」→ 直传到 B 指定目录
- [ ] 打包模式生成可 curl 的 URL;B 机 pull 成功并可解压
- [ ] 源=目标、非法路径返回 400
- [ ] pytest 覆盖校验与 token store
@@ -0,0 +1,44 @@
# IP 服务器自动补全站点域名 — 设计
**日期**: 2026-06-24
## 背景与目标
部分子机 SSH 地址(`servers.domain`)登记为 IP,列表「地址」列无法展示可点击站点链接。运维曾用手工脚本写 `extra_attrs.site_url`;需每日自动补全。
**目标**:北京时间每天 23:30,对「domain 为 IP 且尚无站点域名」的服务器 SSH 探测 nginx 站点名,写入 `extra_attrs.site_url`(不改 SSH 连接字段)。
## 方案对比
| 方案 | 优点 | 缺点 |
|------|------|------|
| A. SSH 读 Baota/nginx vhost | 与 detect-path 一致,无面板依赖 | 需 SSH 凭据 |
| B. 宝塔 API list_sites | 结构化 | 需先 bootstrap 面板凭据,覆盖不全 |
| C. 仅读 target_path 推断 | 无 SSH | 大量机器 target_path 仍为默认 |
**选定 A**:复用现有 SSH 池与 batch job 框架;target_path 仅作优选 hint。
## 数据模型
- **输入**`Server.domain` 为 IPv4/IPv6`resolve_server_site_host(server)` 为空。
- **输出**`extra_attrs.site_url = "<hostname>"`(裸域名,与现有运维脚本一致)。
- **不修改**`domain`SSH 仍走 IP)。
## 接口与任务
- 新 batch op`detect-domain`
- 新后台 loop`ip_domain_detect_loop`60s 轮询 cron
- 配置:`IP_DOMAIN_DETECT_ENABLED`(默认 true)、`IP_DOMAIN_DETECT_CRON`(默认 `30 23 * * *`
## 安全与性能
- 远程命令经 `shlex.quote`;只读 nginx 配置。
- 并发沿用 batch `CONCURRENCY=5`;每日 Redis 去重一次。
- 仅 primary worker 执行。
## 验收标准
1. cron `30 23 * * *` 在北京时间 23:30 触发一次 batch。
2. IP-only 且无 site_url 的服务器被纳入;已有 site_url 的跳过。
3. 成功探测后列表 `#/servers` 地址列 IP 下出现可点击域名。
4. pytest 覆盖 cron、候选筛选、命令构建。
@@ -266,11 +266,11 @@ Header: `X-API-Key` 或 queryAgent 脚本约定)。
| `SshKeyPreset` | `ssh_key_presets` | SSH 密钥模板 | `encrypted_private_key` |
| `PushSchedule` | `push_schedules` | 定时任务 | `cron_expr`, `server_ids`, `schedule_type` push/script |
| `PushRetryJob` | `push_retry_jobs` | 失败重试 | 指数退避, `max_retries=3` |
| `AuditLog` | `audit_logs` | 审计 | `action`, `target_*`, `ip_address` |
| `AuditLog` | `audit_logs` | 审计**7 天**自动清理) | `action`, `target_*`, `ip_address` |
| `Script` | `scripts` | 脚本库 | `content`, `category` |
| `ScriptExecution` | `script_executions` | 执行记录 | `results` JSON, `status` |
| `DbCredential` | `db_credentials` | DB 凭据 | 脚本内 `$DB_*` 替换 |
| `AlertLog` | `alert_logs` | 告警历史 | `is_recovery` |
| `AlertLog` | `alert_logs` | 告警历史**7 天**自动清理) | `is_recovery` |
| `SshSession` | `ssh_sessions` | WebSSH 会话 | UUID `id` |
| `CommandLog` | `command_logs` | 终端命令 | `command` ≤2000 字符 |
| `QuickCommand` | `quick_commands` | 终端快捷命令 | `sort_order` |
@@ -314,6 +314,8 @@ Agent(60s) → POST /api/agent/heartbeat
| `retry_runner` | 300s | `background/retry_runner.py` | `push_retry_jobs` SKIP LOCKED 重试 |
| `ip_allowlist_refresh` | 7200s | `background/ip_allowlist_refresh.py` | 订阅 URL 更新白名单 |
| `upload_staging_cleanup` | 3600s | `background/upload_staging_cleanup.py` | 清理 `/tmp/nexus_upload_*` >24h |
| `alert_log_purge` | 24h | `background/alert_log_purge.py` | 清理 `alert_logs` >7d |
| `audit_log_purge` | 24h | `background/audit_log_purge.py` | 清理 `audit_logs` >7d |
---
@@ -340,7 +342,7 @@ Agent(60s) → POST /api/agent/heartbeat
### 9.3 服务器 `/api/servers`
CRUD、stats、categories、logs、CSV import/export、batch agent install/upgrade/uninstall、**batch detect-path**SSH 搜索 `workerman.bat` 自动写入 `target_path`)、health check、files-capability、setup-files-sudo、单台 agent 操作。
CRUD、stats、categories、logs、CSV import/export、batch agent install/upgrade/uninstall、**batch detect-path**SSH `/www/wwwroot` 下搜索 `crmeb` 目录写入 `target_path`)、health check、files-capability、setup-files-sudo、单台 agent 操作。
### 9.4 同步 `/api/sync``sync_v2.py`,核心)
@@ -537,7 +539,7 @@ main.ts → App.vue(侧栏/搜索)→ router-view
| **连接失败列表** | `GET /servers/pending` |
| **失败重试 / 删除** | `POST /servers/pending/{id}/retry``DELETE /servers/pending/{id}` |
| 批量选择 | 安装/升级/卸载 Agent、**检测目标路径**、健康检查、删除 |
| **检测目标路径** | `POST /servers/batch/detect-path` — Body: `{ server_ids }`SSH 在 `/www/wwwroot` `workerman.bat`,将所在目录写入 `target_path`;结果弹窗逐台展示 |
| **检测目标路径** | `POST /servers/batch/detect-path` — Body: `{ server_ids }`SSH 在 `/www/wwwroot`搜索 `crmeb` 目录写入 `target_path`;结果弹窗逐台展示 |
| **列表快捷编辑** | 目标路径列 ✏️ → `PUT /servers/{id}` `{ target_path }` |
| CSV 导出 | 客户端生成(无密码列) |
| CSV 导入 | `POST /servers/import` FormData |
@@ -569,6 +571,16 @@ main.ts → App.vue(侧栏/搜索)→ router-view
| Editor | Monaco`POST /sync/read-file|write-file` |
| 权限 | files-capability 检测、sudo 提示 |
| 热键 | 复制/粘贴/删除(`useFilesHotkeys` |
| 跨机传输 | 工具栏/右键跳转 `#/server-transfer`(见 §12.4.1 |
### 12.4.1 ServerTransferPage `/server-transfer`
| 能力 | 说明 |
|------|------|
| 源机浏览 | `POST /sync/browse`,多选 + 手动绝对路径 |
| 直传 | `POST /sync/server-transfer`(单文件 ≤500MB |
| 打包投递 | `POST /sync/server-transfer/deliver` + 可选解压 |
| 深链 | `?source=&path=&paths=` 自文件页预填 |
### 12.5 PushPage `/push`
@@ -0,0 +1,53 @@
# Codex 审批代理修复说明(2026-07-08)
## 问题
当前线程的高权限命令审批使用 `auto_review`,它会请求 `codex-auto-review` 模型。
你的 CC Switch / custom provider 当前只支持 `gpt-5.5`,不支持 `codex-auto-review`,所以 SSH、scp、联网验证都会被拒绝。
## 修复方式
把当前线程的审批人从:
```json
"approvalsReviewer": "auto_review"
```
改成:
```json
"approvalsReviewer": "user"
```
这样以后高权限命令会弹给你手动确认,而不是走坏掉的自动审核模型。
## 我已生成脚本
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1
```
## 运行方法
在 PowerShell 里执行:
```powershell
Set-ExecutionPolicy -Scope Process Bypass
& "C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\fix-codex-approval-reviewer.ps1"
```
脚本会自动备份:
```text
C:\Users\uzuma\.codex\.codex-global-state.json.bak-switch-approval-时间戳
```
## 运行后
建议重启 Codex,或者关闭再打开当前线程。然后我就可以再次尝试:
```cmd
python scripts\publish_release_bundle.py --remote nexus --tarball ... --sha256 ...
```
届时应该会弹出手动审批,而不是再报 `codex-auto-review` 不支持。
@@ -0,0 +1,380 @@
# Nexus API / Service / 数据库调用链索引报告
生成时间:2026-07-07
## 1. 总览
| 项目 | 数量 |
| --- | --- |
| API 路由 | 214 |
| Service 方法 | 324 |
| 调用边 | 16780 |
| 数据库调用点 | 437 |
| Redis 调用点 | 729 |
| SSH/远程命令调用点 | 346 |
| 宝塔相关流 | 183 |
## 2. API 方法分布
| HTTP 方法 | 数量 |
| --- | --- |
| POST | 104 |
| GET | 77 |
| PUT | 18 |
| DELETE | 13 |
| PATCH | 2 |
## 3. API 风险标签分布
| 风险标签 | 路由数 |
| --- | --- |
| file | 37 |
| ssh | 35 |
| script | 34 |
| btpanel | 28 |
| install | 18 |
| login | 11 |
| terminal | 7 |
## 4. 高优先级 API 路由(按风险标签/服务调用筛选)
| 方法 | 路径 | 风险标签 | 入口 | 函数 | Service 调用候选 |
| --- | --- | --- | --- | --- | --- |
| POST | /script-callback | script | server/api/agent.py:245 | script_job_callback | |
| GET | /nodes | script | server/api/assets.py:126 | list_nodes | |
| GET | /ssh-sessions | script,ssh | server/api/assets.py:230 | list_ssh_sessions | |
| GET | /command-logs | script,ssh | server/api/assets.py:282 | list_command_logs | |
| GET | /login-access | login | server/api/auth.py:116 | login_access | |
| POST | /login | login | server/api/auth.py:128 | login | service.login |
| POST | /refresh | | server/api/auth.py:173 | refresh_token | service.refresh_token |
| POST | /logout | | server/api/auth.py:214 | logout | service.logout_by_token |
| POST | /totp/setup | | server/api/auth.py:235 | setup_totp | service.setup_totp |
| POST | /totp/enable | | server/api/auth.py:258 | enable_totp | service.enable_totp |
| POST | /totp/disable | | server/api/auth.py:280 | disable_totp | service.disable_totp |
| PUT | /password | login | server/api/auth.py:303 | change_password | AuthService, auth_service._delete_all_refresh_tokens, AuthService._verify_totp |
| POST | /webssh-token | ssh | server/api/auth.py:363 | issue_webssh_token | service.create_webssh_token |
| GET | /me | login | server/api/auth.py:385 | get_me | |
| GET | /servers | btpanel | server/api/btpanel.py:60 | list_bt_servers | |
| GET | /servers/{server_id}/config | btpanel,install,script,ssh | server/api/btpanel.py:68 | get_bt_config | |
| PUT | /servers/{server_id}/config | btpanel | server/api/btpanel.py:81 | update_bt_config | |
| GET | /settings | btpanel | server/api/btpanel.py:103 | get_bt_global_settings | |
| PUT | /settings | btpanel | server/api/btpanel.py:111 | update_bt_global_settings | |
| POST | /servers/{server_id}/bootstrap | btpanel | server/api/btpanel.py:129 | bootstrap_bt_server | |
| POST | /servers/batch-bootstrap | btpanel | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | |
| POST | /servers/{server_id}/login-url | btpanel,login | server/api/btpanel.py:166 | create_bt_login_url | |
| GET | /servers/{server_id}/system/total | btpanel | server/api/btpanel.py:183 | bt_system_total | |
| GET | /servers/{server_id}/system/disk | btpanel | server/api/btpanel.py:191 | bt_system_disk | |
| GET | /servers/{server_id}/system/network | btpanel | server/api/btpanel.py:199 | bt_system_network | |
| GET | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:207 | bt_list_sites | |
| POST | /servers/{server_id}/sites/start | btpanel | server/api/btpanel.py:215 | bt_site_start | |
| POST | /servers/{server_id}/sites/stop | btpanel | server/api/btpanel.py:228 | bt_site_stop | |
| GET | /servers/{server_id}/sites/php-versions | btpanel | server/api/btpanel.py:241 | bt_php_versions | |
| POST | /servers/{server_id}/sites | btpanel | server/api/btpanel.py:249 | bt_create_site | |
| GET | /servers/{server_id}/sites/{site_id}/domains | btpanel | server/api/btpanel.py:280 | bt_list_domains | |
| POST | /servers/{server_id}/domains | btpanel | server/api/btpanel.py:293 | bt_add_domain | |
| POST | /servers/{server_id}/domains/delete | btpanel | server/api/btpanel.py:306 | bt_delete_domain | |
| GET | /servers/{server_id}/ssl/sites | btpanel | server/api/btpanel.py:319 | bt_ssl_sites | |
| POST | /servers/{server_id}/ssl/set | btpanel | server/api/btpanel.py:327 | bt_set_ssl | |
| POST | /servers/{server_id}/ssl/apply | btpanel | server/api/btpanel.py:345 | bt_apply_ssl | |
| GET | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:358 | bt_list_databases | |
| POST | /servers/{server_id}/databases | btpanel | server/api/btpanel.py:366 | bt_create_database | |
| POST | /servers/{server_id}/databases/password | btpanel | server/api/btpanel.py:384 | bt_db_password | |
| POST | /servers/{server_id}/databases/backup | btpanel | server/api/btpanel.py:397 | bt_db_backup | |
| GET | /servers/{server_id}/crontab | btpanel | server/api/btpanel.py:410 | bt_crontab | |
| POST | /servers/{server_id}/services | btpanel | server/api/btpanel.py:418 | bt_service_admin | _svc.service_admin |
| GET | '' | script | server/api/execution_records.py:20 | list_records | |
| GET | /{record_id} | script | server/api/execution_records.py:41 | get_record_detail | |
| GET | /browse | file,ssh | server/api/files.py:40 | browse_directory_get | |
| GET | /status | install | server/api/install.py:972 | install_status | |
| GET | /env-check | install | server/api/install.py:1006 | env_check | |
| POST | /test-credentials | install | server/api/install.py:1097 | test_credentials | |
| GET | /connection-check | file,install | server/api/install.py:1113 | connection_check | |
| POST | /init-db | install | server/api/install.py:1153 | init_db | |
| POST | /create-admin | install | server/api/install.py:1327 | create_admin | |
| POST | /lock | install | server/api/install.py:1426 | lock_install | |
| GET | /state | file,install | server/api/install.py:1469 | get_install_state | |
| GET | / | script | server/api/scripts.py:31 | list_scripts | service.list_scripts |
| GET | /exec-config | script | server/api/scripts.py:42 | script_exec_config | |
| GET | /credentials | script | server/api/scripts.py:56 | list_credentials | service.list_credentials |
| POST | /credentials | script | server/api/scripts.py:66 | create_credential | service.create_credential |
| PUT | /credentials/{id} | script | server/api/scripts.py:90 | update_credential | |
| DELETE | /credentials/{id} | script | server/api/scripts.py:131 | delete_credential | service.delete_credential |
| GET | /executions | script | server/api/scripts.py:152 | list_executions | service.list_executions |
| POST | /executions/{id}/stop | script | server/api/scripts.py:167 | stop_execution | service.stop_execution, service.get_execution_detail |
| POST | /executions/{id}/mark-stuck | script | server/api/scripts.py:181 | mark_execution_stuck | service.mark_execution_stuck, service.get_execution_detail |
| POST | /executions/{id}/retry | script | server/api/scripts.py:198 | retry_execution | service._parse_execution_meta, service.get_execution, service.get_execution_detail, service.retry_execution |
| GET | /executions/{id} | script | server/api/scripts.py:238 | get_execution | service.get_execution_detail |
| GET | /{id} | script | server/api/scripts.py:258 | get_script | service.get_script |
| POST | / | script | server/api/scripts.py:271 | create_script | service.create_script |
| PUT | /{id} | script | server/api/scripts.py:295 | update_script | service.get_script, service.update_script |
| DELETE | /{id} | script | server/api/scripts.py:325 | delete_script | service.get_script, service.delete_script |
| POST | /exec | script | server/api/scripts.py:364 | execute_command | service.get_execution_detail, service.execute_command |
| GET | / | script | server/api/search.py:32 | global_search | |
| GET | / | script | server/api/servers.py:94 | list_servers | |
| GET | /terminal-search-history | terminal | server/api/servers.py:436 | get_terminal_search_history | |
| PUT | /terminal-search-history | terminal | server/api/servers.py:454 | update_terminal_search_history | |
| GET | /logs | script | server/api/servers.py:491 | get_all_sync_logs | |
| GET | /meta/api_base_url | install | server/api/servers.py:561 | get_api_base_url | |
| GET | /import/template | file | server/api/servers.py:570 | download_import_template | |
| POST | /import | file | server/api/servers.py:589 | import_servers | service.create_server |
| POST | /batch/install-agent | install | server/api/servers.py:832 | batch_install_agent | |
| POST | /batch/detect-path | ssh | server/api/servers.py:850 | batch_detect_path | |
| POST | /batch/uninstall-agent | install | server/api/servers.py:859 | batch_uninstall_agent | |
| GET | /pending | script,ssh | server/api/servers.py:977 | list_pending_servers | |
| POST | /add-by-ip | login,ssh | server/api/servers.py:1099 | add_server_by_ip | |
| GET | /{id} | | server/api/servers.py:1334 | get_server | service.get_server |
| GET | /{id}/metrics | | server/api/servers.py:1362 | get_server_metrics | service.get_server |
| GET | /{id}/files-capability | file,ssh | server/api/servers.py:1407 | get_files_capability | service.get_server |
| POST | /{id}/setup-files-sudo | file,install,ssh | server/api/servers.py:1422 | setup_files_sudo | service.get_server |
| POST | / | file,ssh | server/api/servers.py:1463 | create_server | service.create_server |
| PUT | /{id} | file,ssh | server/api/servers.py:1537 | update_server | service.get_server, service.update_server |
| DELETE | /{id} | | server/api/servers.py:1629 | delete_server | service.get_server, service.delete_server |
| POST | /{id}/agent-key | | server/api/servers.py:1659 | generate_agent_api_key | service.get_server, service.update_server |
| POST | /{id}/install-agent | install,ssh | server/api/servers.py:1711 | install_agent_remote | service.get_server, service.update_server |
| POST | /{id}/uninstall-agent | file,install,ssh | server/api/servers.py:1818 | uninstall_agent_remote | service.get_server |
| POST | /{id}/agent-install-cmd | install | server/api/servers.py:1897 | get_agent_install_cmd | service.get_server |
| POST | /{id}/upgrade-agent | ssh | server/api/servers.py:1965 | upgrade_agent | service.get_server |
| POST | /{id}/agent-diagnose | ssh | server/api/servers.py:2067 | agent_diagnose | service.get_server |
| POST | /check | ssh | server/api/servers.py:2105 | check_servers | |
| GET | /ip-allowlist | login,script | server/api/settings.py:168 | get_ip_allowlist | |
| GET | /bing-wallpaper | file | server/api/settings.py:307 | bing_wallpaper | |
| PUT | /{id} | script | server/api/settings.py:476 | update_schedule | |
| GET | / | ssh | server/api/settings.py:684 | list_ssh_key_presets | |
| POST | / | ssh | server/api/settings.py:702 | create_ssh_key_preset | |
| PUT | /{id} | ssh | server/api/settings.py:742 | update_ssh_key_preset | |
| POST | /{id}/reveal | ssh | server/api/settings.py:795 | reveal_ssh_key_preset | |
| DELETE | /{id} | ssh | server/api/settings.py:828 | delete_ssh_key_preset | |
| GET | / | script | server/api/settings.py:856 | list_alert_history | |
| GET | /ops-patrol/status | install | server/api/settings.py:1035 | ops_patrol_status | |
| POST | /ops-patrol/sync-telegram | file | server/api/settings.py:1056 | ops_patrol_sync_telegram | |
| POST | /ip-allowlist/parse-subscription | script | server/api/settings.py:1398 | parse_subscription | |
| POST | /ip-allowlist | login,script | server/api/settings.py:1503 | set_ip_allowlist | |
| POST | /ip-allowlist/toggle | login | server/api/settings.py:1569 | toggle_ip_allowlist | |
| POST | /ip-allowlist/manual | login | server/api/settings.py:1596 | add_manual_ips | |
| DELETE | /ip-allowlist/ip | login | server/api/settings.py:1632 | remove_allowlist_ip | |
| POST | /files | file | server/api/sync_v2.py:80 | sync_files | |
| POST | /preview | file | server/api/sync_v2.py:128 | preview_sync | |
| POST | /file-ops | file,ssh | server/api/sync_v2.py:161 | file_operation | |
| POST | /file-clipboard | file,ssh | server/api/sync_v2.py:228 | apply_file_clipboard | |
| POST | /browse | file,ssh | server/api/sync_v2.py:291 | browse_directory | |
| POST | /browse-local | file | server/api/sync_v2.py:322 | browse_local_directory | |
| POST | /local-file-ops | file | server/api/sync_v2.py:361 | local_file_operation | |
| POST | /local-file-preview | file | server/api/sync_v2.py:432 | local_file_preview | |
## 5. 典型调用链样例
| API | 入口 | 标签 | 调用链候选 |
| --- | --- | --- | --- |
| POST /script-callback | server/api/agent.py:245 | script | 未匹配到 service_calls |
| GET /nodes | server/api/assets.py:126 | script | 未匹配到 service_calls |
| GET /ssh-sessions | server/api/assets.py:230 | script,ssh | 未匹配到 service_calls |
| GET /command-logs | server/api/assets.py:282 | script,ssh | 未匹配到 service_calls |
| GET /login-access | server/api/auth.py:116 | login | 未匹配到 service_calls |
| POST /login | server/api/auth.py:128 | login | AuthService.login(DB×7;Redis×15;SSH×2) |
| PUT /password | server/api/auth.py:303 | login | AuthService.__init__(DB×7;Redis×15;SSH×2) -> AuthService.refresh_token(DB×7;Redis×15;SSH×2) -> AuthService._verify_totp(DB×7;Redis×15;SSH×2) |
| POST /webssh-token | server/api/auth.py:363 | ssh | AuthService.create_webssh_token(DB×7;Redis×15;SSH×2) |
| GET /me | server/api/auth.py:385 | login | 未匹配到 service_calls |
| GET /servers | server/api/btpanel.py:60 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/config | server/api/btpanel.py:68 | script,ssh,btpanel,install | 未匹配到 service_calls |
| PUT /servers/{server_id}/config | server/api/btpanel.py:81 | btpanel | 未匹配到 service_calls |
| GET /settings | server/api/btpanel.py:103 | btpanel | 未匹配到 service_calls |
| PUT /settings | server/api/btpanel.py:111 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/bootstrap | server/api/btpanel.py:129 | btpanel | 未匹配到 service_calls |
| POST /servers/batch-bootstrap | server/api/btpanel.py:148 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/login-url | server/api/btpanel.py:166 | login,btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/total | server/api/btpanel.py:183 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/disk | server/api/btpanel.py:191 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/system/network | server/api/btpanel.py:199 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites | server/api/btpanel.py:207 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites/start | server/api/btpanel.py:215 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites/stop | server/api/btpanel.py:228 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites/php-versions | server/api/btpanel.py:241 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/sites | server/api/btpanel.py:249 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/sites/{site_id}/domains | server/api/btpanel.py:280 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/domains | server/api/btpanel.py:293 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/domains/delete | server/api/btpanel.py:306 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/ssl/sites | server/api/btpanel.py:319 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/ssl/set | server/api/btpanel.py:327 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/ssl/apply | server/api/btpanel.py:345 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/databases | server/api/btpanel.py:358 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases | server/api/btpanel.py:366 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases/password | server/api/btpanel.py:384 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/databases/backup | server/api/btpanel.py:397 | btpanel | 未匹配到 service_calls |
| GET /servers/{server_id}/crontab | server/api/btpanel.py:410 | btpanel | 未匹配到 service_calls |
| POST /servers/{server_id}/services | server/api/btpanel.py:418 | btpanel | BtPanelService.service_admin(DB×7;Redis×16;SSH×7) |
| GET '' | server/api/execution_records.py:20 | script | 未匹配到 service_calls |
| GET /{record_id} | server/api/execution_records.py:41 | script | 未匹配到 service_calls |
| GET /browse | server/api/files.py:40 | file,ssh | 未匹配到 service_calls |
| GET /connection-check | server/api/install.py:1113 | file,install | 未匹配到 service_calls |
| GET /state | server/api/install.py:1469 | file,install | 未匹配到 service_calls |
| GET / | server/api/scripts.py:31 | script | ScriptService.list_scripts(DB×9;Redis×13;SSH×9) |
| GET /exec-config | server/api/scripts.py:42 | script | 未匹配到 service_calls |
| GET /credentials | server/api/scripts.py:56 | script | ScriptService.list_credentials(DB×9;Redis×13;SSH×9) |
| POST /credentials | server/api/scripts.py:66 | script | ScriptService.create_credential(DB×9;Redis×13;SSH×9) |
| PUT /credentials/{id} | server/api/scripts.py:90 | script | 未匹配到 service_calls |
| DELETE /credentials/{id} | server/api/scripts.py:131 | script | ScriptService.delete_credential(DB×9;Redis×13;SSH×9) |
| GET /executions | server/api/scripts.py:152 | script | ScriptService.list_executions(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/stop | server/api/scripts.py:167 | script | ScriptService.stop_execution(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/mark-stuck | server/api/scripts.py:181 | script | ScriptService.mark_execution_stuck(DB×9;Redis×13;SSH×9) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| POST /executions/{id}/retry | server/api/scripts.py:198 | script | ScriptService._parse_execution_meta(DB×9;Redis×13;SSH×9) -> get_execution_record_detail(Redis×6) -> ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.retry_executio... |
| GET /executions/{id} | server/api/scripts.py:238 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) |
| GET /{id} | server/api/scripts.py:258 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) |
| POST / | server/api/scripts.py:271 | script | ScriptService.create_script(DB×9;Redis×13;SSH×9) |
| PUT /{id} | server/api/scripts.py:295 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.update_script(DB×9;Redis×13;SSH×9) |
| DELETE /{id} | server/api/scripts.py:325 | script | ScriptService.get_script(DB×9;Redis×13;SSH×9) -> ScriptService.delete_script(DB×9;Redis×13;SSH×9) |
| POST /exec | server/api/scripts.py:364 | script | ScriptService.get_execution(DB×9;Redis×13;SSH×9) -> ScriptService.execute_command(DB×9;Redis×13;SSH×9) |
| GET / | server/api/search.py:32 | script | 未匹配到 service_calls |
| GET / | server/api/servers.py:94 | script | 未匹配到 service_calls |
| GET /terminal-search-history | server/api/servers.py:436 | terminal | 未匹配到 service_calls |
| PUT /terminal-search-history | server/api/servers.py:454 | terminal | 未匹配到 service_calls |
| GET /logs | server/api/servers.py:491 | script | 未匹配到 service_calls |
| GET /import/template | server/api/servers.py:570 | file | 未匹配到 service_calls |
| POST /import | server/api/servers.py:589 | file | ServerService.create_server(DB×2;Redis×3;SSH×2) |
| POST /batch/detect-path | server/api/servers.py:850 | ssh | 未匹配到 service_calls |
| GET /pending | server/api/servers.py:977 | script,ssh | 未匹配到 service_calls |
| POST /add-by-ip | server/api/servers.py:1099 | login,ssh | 未匹配到 service_calls |
| GET /{id}/files-capability | server/api/servers.py:1407 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/setup-files-sudo | server/api/servers.py:1422 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST / | server/api/servers.py:1463 | file,ssh | ServerService.create_server(DB×2;Redis×3;SSH×2) |
| PUT /{id} | server/api/servers.py:1537 | file,ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) |
| POST /{id}/install-agent | server/api/servers.py:1711 | ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) -> ServerService.update_server(DB×2;Redis×3;SSH×2) |
| POST /{id}/uninstall-agent | server/api/servers.py:1818 | file,ssh,install | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/upgrade-agent | server/api/servers.py:1965 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /{id}/agent-diagnose | server/api/servers.py:2067 | ssh | BtPanelService.get_server(DB×7;Redis×16;SSH×7) |
| POST /check | server/api/servers.py:2105 | ssh | 未匹配到 service_calls |
| GET /ip-allowlist | server/api/settings.py:168 | login,script | 未匹配到 service_calls |
| GET /bing-wallpaper | server/api/settings.py:307 | file | 未匹配到 service_calls |
| PUT /{id} | server/api/settings.py:476 | script | 未匹配到 service_calls |
## 6. 宝塔相关流
| 类型 | 位置 | 函数 | 标签/Key | 摘要 |
| --- | --- | --- | --- | --- |
| api | server/api/btpanel.py:60 | list_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_server_statuses', '_svc'], 'fi |
| api | server/api/btpanel.py:68 | get_bt_config | btpanel,install,script,ssh | {'auth_required_inferred': True, 'calls': ['router.get', 'Query', 'Depends', 'Depends', '_svc.get_config', '_http_error' |
| api | server/api/btpanel.py:81 | update_bt_config | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_config', '_http_error', '_sv |
| api | server/api/btpanel.py:103 | get_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.get_global_settings', '_svc'], 'fil |
| api | server/api/btpanel.py:111 | update_bt_global_settings | btpanel | {'auth_required_inferred': True, 'calls': ['router.put', 'Depends', 'Depends', '_svc.update_global_settings', '_http_err |
| api | server/api/btpanel.py:129 | bootstrap_bt_server | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.bootstrap_server', '_http_error', |
| api | server/api/btpanel.py:148 | batch_bootstrap_bt_servers | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.batch_bootstrap', '_http_error', ' |
| api | server/api/btpanel.py:166 | create_bt_login_url | btpanel,login | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_login_url', '_http_error', |
| api | server/api/btpanel.py:183 | bt_system_total | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_total', '_http_error', '_svc |
| api | server/api/btpanel.py:191 | bt_system_disk | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_disk', '_http_error', '_svc' |
| api | server/api/btpanel.py:199 | bt_system_network | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.system_network', '_http_error', '_s |
| api | server/api/btpanel.py:207 | bt_list_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_sites', '_http_error', '_svc'] |
| api | server/api/btpanel.py:215 | bt_site_start | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_start', '_http_error', '_svc' |
| api | server/api/btpanel.py:228 | bt_site_stop | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.site_stop', '_http_error', '_svc'] |
| api | server/api/btpanel.py:241 | bt_php_versions | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.php_versions', '_http_error', '_svc |
| api | server/api/btpanel.py:249 | bt_create_site | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', 'json.dumps', '_svc.create_site', '_http |
| api | server/api/btpanel.py:280 | bt_list_domains | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_domains', '_http_error', '_svc |
| api | server/api/btpanel.py:293 | bt_add_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.add_domain', '_http_error', 'body. |
| api | server/api/btpanel.py:306 | bt_delete_domain | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.delete_domain', '_http_error', 'bo |
| api | server/api/btpanel.py:319 | bt_ssl_sites | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_ssl_sites', '_http_error', '_s |
| api | server/api/btpanel.py:327 | bt_set_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.set_ssl', '_http_error', 'body.mod |
| api | server/api/btpanel.py:345 | bt_apply_ssl | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.apply_letsencrypt', '_http_error', |
| api | server/api/btpanel.py:358 | bt_list_databases | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_databases', '_http_error', '_s |
| api | server/api/btpanel.py:366 | bt_create_database | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.create_database', '_http_error', ' |
| api | server/api/btpanel.py:384 | bt_db_password | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.reset_database_password', '_http_e |
| api | server/api/btpanel.py:397 | bt_db_backup | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.backup_database', '_http_error', ' |
| api | server/api/btpanel.py:410 | bt_crontab | btpanel | {'auth_required_inferred': True, 'calls': ['router.get', 'Depends', 'Depends', '_svc.list_crontab', '_http_error', '_svc |
| api | server/api/btpanel.py:418 | bt_service_admin | btpanel | {'auth_required_inferred': True, 'calls': ['router.post', 'Depends', 'Depends', '_svc.service_admin', '_http_error', '_s |
| service | server/application/services/btpanel_bootstrap_schedule.py:24 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_AUTO_BOOTSTRAP_ENABLED', None) or 'true'.strip.lower", "getattr(settings, 'BT_PA |
| service | server/application/services/btpanel_bootstrap_schedule.py:29 | | btpanel | {'calls': ['getattr', 'max', 'int', 'min'], 'class': None, 'end_line': 35, 'file': 'server/application/services/btpanel_ |
| service | server/application/services/btpanel_bootstrap_schedule.py:38 | | btpanel,session | {'calls': ['int', 'AsyncSessionLocal', 'ServerRepositoryImpl', 'mark_bootstrap_pending', 'repo.get_by_id', 'bt_panel_con |
| service | server/application/services/btpanel_bootstrap_schedule.py:54 | | btpanel | {'calls': ['is_auto_bootstrap_enabled', 'int', 'mark_servers_bootstrap_pending', 'asyncio.create_task', '_BG_TASKS.add', |
| service | server/application/services/btpanel_bootstrap_schedule.py:68 | | btpanel,session | {'calls': ['AsyncSessionLocal', 'BtPanelService', 'logger.warning', 'svc.bootstrap_server'], 'class': None, 'end_line': |
| service | server/application/services/btpanel_bootstrap_schedule.py:84 | | btpanel,session | {'calls': ['datetime.now', 'bootstrap_batch_size', 'is_auto_bootstrap_enabled', 'AsyncSessionLocal', 'ServerRepositoryIm |
| service | server/application/services/btpanel_service.py:54 | | btpanel | {'calls': ['isinstance'], 'class': None, 'end_line': 55, 'file': 'server/application/services/btpanel_service.py', 'id': |
| service | server/application/services/btpanel_service.py:59 | | btpanel | {'calls': ['ServerRepositoryImpl', 'AuditLogRepositoryImpl'], 'class': 'BtPanelService', 'end_line': 62, 'file': 'server |
| service | server/application/services/btpanel_service.py:64 | | btpanel | {'calls': ['self.servers.get_by_id', 'ValueError'], 'class': 'BtPanelService', 'end_line': 68, 'file': 'server/applicati |
| service | server/application/services/btpanel_service.py:70 | | btpanel | {'calls': ['read_bt_panel_credentials', 'BtPanelClient', 'ValueError'], 'class': 'BtPanelService', 'end_line': 74, 'file |
| service | server/application/services/btpanel_service.py:76 | | btpanel,redis | {'calls': ['get_redis', 'redis.pipeline', 'zip', 'self.servers.get_all', 'pipe.hgetall', 'pipe.execute', 'agent_is_insta |
| service | server/application/services/btpanel_service.py:112 | | btpanel | {'calls': ['self.get_server', 'self._resolve_bt_installed', 'public_bt_panel_status'], 'class': 'BtPanelService', 'end_l |
| service | server/application/services/btpanel_service.py:117 | | btpanel | {'calls': ['get_bt_panel_block', 'block.get', 'block.get', 'merge_bootstrap_fields', 'bt_panel_configured', 'detect_bt_p |
| service | server/application/services/btpanel_service.py:141 | | btpanel | {'calls': ["getattr(settings, 'BT_PANEL_SOURCE_IP', None) or ''.strip", 'self._auto_bootstrap_enabled', 'get_bt_panel_so |
| service | server/application/services/btpanel_service.py:150 | | btpanel | {'calls': ['bt_panel_source_ip.strip', 'self.audit.create', 'self.get_global_settings', 'ValueError', 'self._persist_set |
| service | server/application/services/btpanel_service.py:183 | | btpanel | {'calls': ['SettingRepositoryImpl', 'settings.apply_db_override', 'repo.set', 'publish_setting_change'], 'class': 'BtPan |
| service | server/application/services/btpanel_service.py:194 | | btpanel | {'calls': ['is_auto_bootstrap_enabled'], 'class': 'BtPanelService', 'end_line': 197, 'file': 'server/application/service |
| service | server/application/services/btpanel_service.py:199 | | btpanel | {'calls': ['merge_bt_panel_extra', 'self.get_server', 'self.servers.update', 'self.audit.create', 'self.get_config', 'is |
| service | server/application/services/btpanel_service.py:249 | | btpanel | {'calls': ['bootstrap_lock', 'self._bootstrap_server_locked'], 'class': 'BtPanelService', 'end_line': 265, 'file': 'serv |
| service | server/application/services/btpanel_service.py:267 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self.get_server', 'bt_panel_c |
| service | server/application/services/btpanel_service.py:368 | | btpanel | {'calls': ['ValueError', 'start_batch_job', 'self.list_server_statuses', 'int', 'int', 'ids.append', 's.get'], 'class': |
| service | server/application/services/btpanel_service.py:398 | | btpanel | {'calls': ['self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 421, 'file': 'server/a |
| service | server/application/services/btpanel_service.py:424 | | btpanel | {'calls': ['getattr', 'getattr', 'bool', "getattr(server, 'auth_method', None) or ''.strip", 'bool', 'getattr', 'getattr |
| service | server/application/services/btpanel_service.py:437 | | btpanel,session | {'calls': ['get_bt_panel_block', '_parse_iso', '_utcnow() - checked_at.total_seconds', 'block.get', 'block.get', '_utcno |
| service | server/application/services/btpanel_service.py:447 | | btpanel,session | {'calls': ['merge_bootstrap_fields', 'self.servers.update', 'self.get_server', 'isinstance', '_iso', '_utcnow'], 'class' |
| service | server/application/services/btpanel_service.py:457 | | btpanel,session | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'apply_bootstrap_success', 'self._session_cleanup_ttl_che |
| service | server/application/services/btpanel_service.py:502 | | btpanel | {'calls': ['login_url_lock', 'self._create_login_url_fresh', 'self._audit_login_url_failure'], 'class': 'BtPanelService' |
| service | server/application/services/btpanel_service.py:529 | | btpanel,session | {'calls': ['bt_panel_configured', 'read_bt_panel_credentials', 'self.get_server', 'bt_panel_configured', 'read_bt_panel_ |
| service | server/application/services/btpanel_service.py:585 | | btpanel | {'calls': ['self.audit.create', 'logger.warning', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 607, |
| service | server/application/services/btpanel_service.py:609 | | btpanel | {'calls': ['self._repair_ip_whitelist', 'read_bt_panel_credentials', 'logger.warning', 'logger.warning', 'self.get_serve |
| service | server/application/services/btpanel_service.py:642 | | btpanel | {'calls': ['BtPanelClient', 'isinstance', 'BtPanelApiError', 'int', 'client.post', 'data.get', 'time.time', 'data.get', |
| service | server/application/services/btpanel_service.py:674 | | btpanel | {'calls': ['self._client', 'self.get_server', 'client.post', 'str', 'self.get_server', 'self._client.post', 'self._repai |
| service | server/application/services/btpanel_service.py:692 | | btpanel | {'calls': ['get_bt_panel_source_ip', 'str.strip', 'str.strip', 'logger.info', 'logger.warning', 'apply_bootstrap_success |
| service | server/application/services/btpanel_service.py:722 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 723, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:725 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 726, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:728 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 729, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:732 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 738, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:740 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 741, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:743 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 744, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:747 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 748, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:750 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:763 | | btpanel | {'calls': ['self.proxy', 'str'], 'class': 'BtPanelService', 'end_line': 768, 'file': 'server/application/services/btpane |
| service | server/application/services/btpanel_service.py:770 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 771, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:773 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 774, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:777 | | btpanel | {'calls': ['self.list_sites'], 'class': 'BtPanelService', 'end_line': 778, 'file': 'server/application/services/btpanel_ |
| service | server/application/services/btpanel_service.py:780 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:792 | | btpanel,crypto | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 793, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:796 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 801, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:803 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps', 'payload.get'], 'class': 'BtPanelService', 'end_ |
| service | server/application/services/btpanel_service.py:815 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 816, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:818 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 819, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:822 | | btpanel | {'calls': ['self.proxy'], 'class': 'BtPanelService', 'end_line': 823, 'file': 'server/application/services/btpanel_servi |
| service | server/application/services/btpanel_service.py:826 | | btpanel | {'calls': ['self.proxy', 'self.audit.create', 'AuditLog', 'json.dumps'], 'class': 'BtPanelService', 'end_line': 836, 'fi |
| service | server/application/services/server_batch_service.py:774 | | btpanel,session | {'calls': ['str', 'server_map.get', '_run_with_semaphore', 'live.get', 'result_item_dict', 'result.get', 'result.get', ' |
| service | server/application/services/server_batch_service.py:779 | | btpanel,session | {'calls': ['server_map.get', 'result_item_dict', 'result.get', 'result.get', 'result_item_dict', 'AsyncSessionLocal', 'B |
| service | server/application/services/server_file_transfer_service.py:397 | | btpanel,token | {'calls': ['read_bt_panel_credentials', 'signed_download_url', 'ServerTransferError', 'ServerTransferError', 'normalize_ |
| service | server/application/services/server_file_transfer_service.py:497 | | btpanel | {'calls': ['_assert_distinct_servers', 'int', '_get_server', 'bt_panel_configured', 'read_bt_panel_credentials', 'create |
| redis | tests/test_btpanel_client.py:39 | test_merge_bt_panel_extra_encrypts_key | enc:secret,enc:{s} | {'file': 'tests/test_btpanel_client.py', 'function': 'test_merge_bt_panel_extra_encrypts_key', 'id': 'tests/test_btpanel |
| redis | tests/test_btpanel_get_config.py:64 | test_bootstrap_lock_serializes_same_server | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server', 'id': 'tests/test |
| redis | tests/test_btpanel_get_config.py:67 | test_bootstrap_lock_serializes_same_server.worker | | {'file': 'tests/test_btpanel_get_config.py', 'function': 'test_bootstrap_lock_serializes_same_server.worker', 'id': 'tes |
| redis | tests/test_btpanel_login_url.py:19 | _FakeRedis.get | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_FakeRedis.get', 'id': 'tests/test_btpanel_login_url.py::_FakeR |
| redis | tests/test_btpanel_login_url.py:27 | _noop_login_url_lock | | {'file': 'tests/test_btpanel_login_url.py', 'function': '_noop_login_url_lock', 'id': 'tests/test_btpanel_login_url.py:: |
| redis | tests/test_btpanel_login_url.py:32 | test_create_login_url_bootstraps_when_unconfigured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_bootstraps_when_unconfigured', 'id': 'tes |
| redis | tests/test_btpanel_login_url.py:78 | test_create_login_url_does_not_reuse_single_use_tokens | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_does_not_reuse_single_use_tokens', 'id': |
| redis | tests/test_btpanel_login_url.py:119 | test_create_login_url_skips_bootstrap_when_configured | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_skips_bootstrap_when_configured', 'id': ' |
| redis | tests/test_btpanel_login_url.py:161 | test_create_login_url_ensures_session_cleanup_ttl_once_for_configured_server | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_ensures_session_cleanup_ttl_once_for_conf |
| redis | tests/test_btpanel_login_url.py:224 | test_create_login_url_repairs_stale_port_then_uses_api | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_repairs_stale_port_then_uses_api', 'id': |
| redis | tests/test_btpanel_login_url.py:288 | test_create_login_url_falls_back_to_ssh_when_api_unreachable | | {'file': 'tests/test_btpanel_login_url.py', 'function': 'test_create_login_url_falls_back_to_ssh_when_api_unreachable', |
| redis | tests/test_btpanel_server_list.py:23 | test_list_server_statuses_includes_live_online_from_redis | | {'file': 'tests/test_btpanel_server_list.py', 'function': 'test_list_server_statuses_includes_live_online_from_redis', ' |
| redis | tests/test_btpanel_ssh_bootstrap.py:34 | test_mark_bootstrap_pending_sets_now | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_mark_bootstrap_pending_sets_now', 'id': 'tests/test_bt |
| redis | tests/test_btpanel_ssh_bootstrap.py:42 | test_apply_failure_bt_not_installed_goes_installing | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_bt_not_installed_goes_installing', 'id': |
| redis | tests/test_btpanel_ssh_bootstrap.py:50 | test_apply_failure_ssh_auth_is_permanent | | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_failure_ssh_auth_is_permanent', 'id': 'tests/tes |
| redis | tests/test_btpanel_ssh_bootstrap.py:57 | test_apply_success_sets_ready | enc:token123,enc:{s} | {'file': 'tests/test_btpanel_ssh_bootstrap.py', 'function': 'test_apply_success_sets_ready', 'id': 'tests/test_btpanel_s |
| redis | server/api/btpanel.py:46 | _http_error | | {'file': 'server/api/btpanel.py', 'function': '_http_error', 'id': 'server/api/btpanel.py::_http_error::redis', 'key_pat |
| redis | server/infrastructure/btpanel/bootstrap_lock.py:13 | bootstrap_lock | | {'file': 'server/infrastructure/btpanel/bootstrap_lock.py', 'function': 'bootstrap_lock', 'id': 'server/infrastructure/b |
| redis | server/infrastructure/btpanel/bootstrap_state.py:43 | get_bootstrap_block | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block', 'id': 'server/infrastruc |
| redis | server/infrastructure/btpanel/bootstrap_state.py:78 | public_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'public_bootstrap_fields', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:96 | merge_bootstrap_fields | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'merge_bootstrap_fields', 'id': 'server/infrast |
| redis | server/infrastructure/btpanel/bootstrap_state.py:126 | bt_panel_configured_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'bt_panel_configured_from_attrs', 'id': 'server |
| redis | server/infrastructure/btpanel/bootstrap_state.py:135 | is_eligible_for_background_bootstrap | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'is_eligible_for_background_bootstrap', 'id': ' |
| redis | server/infrastructure/btpanel/bootstrap_state.py:155 | apply_bootstrap_success | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_success', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:196 | apply_bootstrap_failure | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'apply_bootstrap_failure', 'id': 'server/infras |
| redis | server/infrastructure/btpanel/bootstrap_state.py:223 | get_bootstrap_block_from_attrs | | {'file': 'server/infrastructure/btpanel/bootstrap_state.py', 'function': 'get_bootstrap_block_from_attrs', 'id': 'server |
| redis | server/infrastructure/btpanel/client.py:29 | raise_if_panel_error | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'raise_if_panel_error', 'id': 'server/infrastructure/btp |
| redis | server/infrastructure/btpanel/client.py:50 | BtPanelClient._load_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._load_cookies', 'id': 'server/infrastruct |
| redis | server/infrastructure/btpanel/client.py:60 | BtPanelClient._save_cookies | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient._save_cookies', 'id': 'server/infrastruct |
| redis | server/infrastructure/btpanel/client.py:64 | BtPanelClient.post | | {'file': 'server/infrastructure/btpanel/client.py', 'function': 'BtPanelClient.post', 'id': 'server/infrastructure/btpan |
| redis | server/infrastructure/btpanel/credentials.py:26 | get_bt_panel_block | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_block', 'id': 'server/infrastructure/ |
| redis | server/infrastructure/btpanel/credentials.py:31 | bt_panel_configured | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'bt_panel_configured', 'id': 'server/infrastructure |
| redis | server/infrastructure/btpanel/credentials.py:36 | get_bt_panel_base_url | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'get_bt_panel_base_url', 'id': 'server/infrastructu |
| redis | server/infrastructure/btpanel/credentials.py:41 | read_bt_panel_credentials | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'read_bt_panel_credentials', 'id': 'server/infrastr |
| redis | server/infrastructure/btpanel/credentials.py:54 | merge_bt_panel_extra | | {'file': 'server/infrastructure/btpanel/credentials.py', 'function': 'merge_bt_panel_extra', 'id': 'server/infrastructur |
## 7. 安全审查优先级建议
1. **宝塔一键登录/session**:优先审查 `server/application/services/btpanel_service.py``server/infrastructure/btpanel/*`,关注 token TTL、Redis lock、并发打开 10 个面板、失败回退和审计日志。
2. **文件管理/归档/解压**:优先审查带 `file` 标签的 API 与 `server/infrastructure/ssh/remote_archive.py`、文件传输路径,关注路径穿越、tar/zip 选项注入、软链、绝对路径。
3. **SSH/远程 shell**:用户确认管理员远程执行 shell 是业务能力,不作为漏洞本身;审查重点应放在鉴权、审计、超时、输出截断、日志脱敏、命令模板边界。
4. **数据库访问**:对 `db_queries.jsonl` 中写操作链路检查权限过滤、事务 commit/rollback、分页上限和多租户隔离。
5. **Redis 锁/缓存**:对 login/session/任务类 Redis key 检查 TTL、锁释放、失败重试与缓存穿透。
@@ -0,0 +1,349 @@
# Nexus API 限速与并发限制盘点
日期:2026-07-07
范围:本地代码上下文快照 `work/nexus_code_context/snapshots/nexus-source-20260707-112042`
说明:本报告只盘点代码内的 API rate limit、429、throttle、并发/批量限制;不包含外部平台(例如 Codex 模型 API)的 429。
## 结论
1. 当前 Nexus 后端没有发现全局 HTTP API 限速中间件。
- 未发现 slowapi / fastapi-limiter / starlette limiter 等依赖或全局 middleware。
- `server/main.py` 主要是安装模式、DB session、安全头、AppAuth、JWT、CORS 等中间件,没有全局 rate limit。
2. 当前会返回 HTTP 429 的后端代码只有两类:
- 管理员登录失败锁定:`/api/auth/login`
- 子机脚本回调限速:`/api/agent/script-callback`
3. 大量“速度限制”其实不是 API rate limit,而是远程 SSH / 同步 / 批量任务的并发保护。
- 这些限制用于避免同时打爆子机、SSH、MySQL、Redis 或 Nexus 自身 worker。
- 用户正常浏览后台 API 不会被这些限制直接 429。
## 1. 全局 API 限速
### 当前状态
没有发现全局限速。
检索点:
- `rate_limit` / `ratelimit` / `throttle` / `slowapi` / `limiter`
- `status_code=429`
- FastAPI middleware
- Docker / Uvicorn worker 配置
### 影响
- 好处:后台操作不会因为普通访问频率触发全局 429。
- 风险:如果公网暴露,普通 API 缺少统一抗刷保护;目前主要依赖登录锁定、鉴权、业务级并发控制。
## 2. 登录接口限速 / 锁定
### 位置
- `server/api/auth.py:127-156`
- `server/application/services/auth_service.py:37-39`
- `server/application/services/auth_service.py:99-107`
- `server/infrastructure/database/admin_repo.py:41-53`
### 规则
```text
MAX_LOGIN_FAILURES = 5
LOCKOUT_MINUTES = 15
```
逻辑:
```text
/api/auth/login
-> AuthService.login()
-> count_recent_failures(username, ip_address, 15)
-> 最近 15 分钟同 username 失败次数 >= 5
-> 返回 reason=account_locked
-> API 层转成 HTTP 429
```
仓库实现实际按 username 统计最近失败次数:
```text
LoginAttempt.username == username
LoginAttempt.success == False
LoginAttempt.attempted_at >= cutoff
```
备注:白名单 IP 会跳过失败计数锁定。
### 影响
这是登录防爆破,不是普通 API 限速。
如果登录时出现 429,含义是账号登录失败过多锁定 15 分钟。
## 3. 脚本回调接口限速
### 位置
- `server/api/agent.py:244-264`
- `server/infrastructure/redis/script_callback_rate.py:14-16`
- `server/infrastructure/redis/script_callback_rate.py:31-44`
### 规则
```text
每 job_id10 次 / 60 秒
每 IP60 次 / 60 秒
```
Redis key
```text
script_cb:job:{job_id}
script_cb:ip:{ip}
```
触发后返回:
```text
HTTP 429 Too many script callback requests
```
### 影响
这是专门保护子机长任务脚本回调的,避免未登录回调接口被刷或同一 job 重复回调过多。
不会影响后台普通 API,也不会影响宝塔一键登录。
## 4. 脚本远程执行并发限制
### 位置
- `server/config.py:66-69`
- `server/api/scripts.py:41-50`
- `server/application/services/script_service.py:254-260`
- `server/application/services/script_service.py:759-761`
### 当前值
```text
SCRIPT_EXEC_BATCH_SIZE = 50
SCRIPT_EXEC_CONCURRENCY = 10
```
含义:
- 一批最多 50 台服务器。
- 每批同时最多 10 个 SSH 执行。
- `/api/scripts/exec-config` 会返回这些值给前端显示。
### 影响
这是远程命令执行的并发保护,不是 HTTP API 限速。
如果一次选很多服务器,会按批次和并发执行,所以看起来“执行速度被限制”。
用户已说明 `script_service` 是管理员远程执行命令功能,任意 shell 本身不是问题;这里仅记录并发限制。
## 5. 同步/推送并发限制
### 位置
- `server/api/schemas.py:224-225`
- `server/application/services/sync_engine_v2.py:39`
- `server/application/services/sync_engine_v2.py:113-127`
### 当前值
API schema
```text
batch_size: 默认 50,范围 1-200
concurrency: 默认 10,范围 1-50
```
服务层硬上限:
```text
MAX_CONCURRENT = 10
concurrency = min(concurrency, MAX_CONCURRENT)
```
### 影响
虽然 API 入参允许 concurrency 到 50,但服务层最终会压到最多 10。
所以同步/推送如果前端或 API 传 50,实际仍最多 10 并发。
这是一个“配置表现不一致”的点。
## 6. 同步校验 verify 并发限制
### 位置
- `server/api/sync_v2.py:1352-1354`
### 当前值
```text
sem = asyncio.Semaphore(5)
```
### 影响
同步校验会最多 5 台并发 SSH 校验。
这也不是 API 429,而是校验任务执行速度保护。
## 7. 批量服务器操作并发限制
### 位置
- `server/application/services/server_batch_service.py:35`
- `server/application/services/server_batch_service.py:427-446`
### 当前值
```text
CONCURRENCY = 5
```
适用操作包括:
- health-check
- detect-path
- detect-domain
- onboard
- install-agent
- upgrade-agent
- uninstall-agent
- bt-panel-bootstrap
### 影响
批量服务器操作最多 5 台并发。
宝塔批量 bootstrap 也走这里。
这不是 HTTP 限速,而是远程任务并发限制。
## 8. SSH 健康检查并发限制
### 位置
- `server/application/server_connectivity.py:124-142`
### 当前值
```text
concurrency = 10
sem = asyncio.Semaphore(max(1, concurrency))
```
### 影响
用于批量 SSH health check,默认 10 并发。
## 9. WebSocket 连接数限制
### 位置
- `server/api/websocket.py:42-53`
### 当前值
```text
MAX_CONNECTIONS = 500
```
超过后关闭连接:
```text
code=4003, reason="Too many connections"
```
### 影响
这不是 HTTP API 限速,是单 worker 内存里的 WebSocket 连接数上限。
## 10. 前端侧并发/节流
### 位置
- `frontend/src/api/index.ts`:只处理后端 429 文案,没有主动限速。
- `frontend/src/utils/filePreload.ts:12`:文件预加载并发 4。
- `frontend/src/composables/push/types.ts:98`:推送预览并发 4。
- `frontend/src/composables/push/usePushPreview.ts:75-76`:按 4 个一批预览。
### 影响
前端没有全局 API throttle/debounce。
只有局部预加载/预览并发保护。
## 11. 部署侧限制
### 位置
- `Dockerfile:43`
当前容器命令:
```text
uvicorn server.main:app --host 0.0.0.0 --port 8600
```
没有发现:
- gunicorn worker 数配置
- uvicorn --workers 配置
- nginx limit_req 配置
- compose 侧 API rate limit
### 影响
当前 Docker 部署默认单 Uvicorn worker。
这不是“限速”,但会影响并发吞吐;远程 SSH 操作如果阻塞资源,也可能让 API 响应变慢。
## 12. 如果要调整,先给 6 个方向
后续如果要改,我建议先选方向,不直接动代码:
### 方案 A:不加全局限速,只调远程任务并发
- 调整脚本执行、同步、批量任务并发。
- 适合内网/自用后台。
- 风险低,不影响普通 API。
### 方案 B:加登录/敏感接口更严格限速
- 保持普通 API 不限速。
- 对登录、令牌刷新、敏感设置、脚本回调加 Redis 限速。
- 安全收益高,误伤较少。
### 方案 C:加全局 Redis 限速,但白名单内网/IP
- 对所有 API 做每 IP / 每用户限速。
- 内网或管理员 IP 放宽。
- 更安全,但可能误伤批量操作和前端轮询。
### 方案 D:按接口分层限速
- 读接口宽松。
- 写接口中等。
- 登录/敏感/远程执行严格。
- 最合理,但实现和测试工作量最大。
### 方案 E:只优化吞吐,不做 rate limit
- 增加 uvicorn/gunicorn workers。
- 调 DB pool、Redis、SSH 线程池/并发。
- 目标是更快,不是防刷。
### 方案 F:把后台远程任务全部队列化
- API 只创建任务,worker 队列执行。
- 支持取消、重试、并发池、优先级。
- 长期最稳,但架构改动最大。
## 当前建议
如果你的目标是“后台 API 不要被限速”,当前代码已经没有全局 API 限速。
真正影响速度的主要是:
1. 同步服务硬上限 `MAX_CONCURRENT = 10`
2. 批量服务器操作 `CONCURRENCY = 5`
3. 脚本执行 `SCRIPT_EXEC_CONCURRENCY = 10``SCRIPT_EXEC_BATCH_SIZE = 50`
4. Docker 默认单 Uvicorn worker。
如果你的目标是“提高批量操作速度”,优先讨论方案 A 或 E。
如果你的目标是“增强公网安全”,优先讨论方案 B 或 D。
@@ -0,0 +1,120 @@
# Nexus → Gitea 白名单同步说明(2026-07-09
## 目标
在 Nexus 里统一管理登录 IP 白名单,并把同一份“合并后的有效 IP/CIDR”同步到 Gitea 前置 Nginx,做到:
- Nexus 登录白名单继续作为主数据源;
- Gitea 不改源码、不改数据库;
- 新增 `/app-v2` 管理面板,可保存配置、预览 Nginx include、手动同步;
- 可开启自动同步:Nexus 白名单变化后自动写入 Gitea 机器并 reload Nginx。
## 调用链
```mermaid
flowchart TD
A[管理员在 Nexus 修改白名单] --> B[server/api/settings.py]
B --> C[ip_allowlist_refresh.do_refresh]
C --> D[settings.login_allowed_ips]
D --> E[gitea_allowlist_service.render_nginx_allowlist]
E --> F[SSH remote_write_bytes 写入 include 文件]
F --> G[nginx -t && systemctl reload nginx]
H[/app-v2 GiteaAllowlistPanel] --> I[GET/POST /api/settings/gitea-allowlist]
I --> E
```
## 新增后端接口
- `GET /api/settings/gitea-allowlist`
返回 Gitea 同步配置、Nexus 当前有效白名单、Nginx 可用 IP/CIDR、被跳过的域名项、include 预览、最近同步状态。
- `POST /api/settings/gitea-allowlist`
保存配置:
- `enabled`
- `auto_sync`
- `server_id`
- `remote_path`
- `reload_command`
- `POST /api/settings/gitea-allowlist/sync`
立即同步到 Gitea 服务器。
## 新增配置项
配置仍写入现有 `settings` 表,不新建表:
```text
gitea_allowlist_enabled
gitea_allowlist_auto_sync
gitea_allowlist_server_id
gitea_allowlist_remote_path
gitea_allowlist_reload_command
gitea_allowlist_last_sync_at
gitea_allowlist_last_sync_ok
gitea_allowlist_last_sync_error
```
默认 include 路径:
```text
/etc/nginx/gitea-allowlist.conf
```
默认 reload 命令:
```bash
nginx -t && systemctl reload nginx
```
## Nginx 接入方式
在 Gitea 的 Nginx `server``location /` 中 include Nexus 生成的文件:
```nginx
location / {
include /etc/nginx/gitea-allowlist.conf;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
## 安全边界
- 只把 IP/CIDR 写成 Nginx `allow` 指令。
- Nexus 白名单里如果有域名,会在 Gitea 同步中跳过并写入注释,因为 Nginx `allow` 不支持域名动态解析。
- `enabled=false` 时生成的 include 不包含 `allow/deny`,不会阻断访问。
- `enabled=true` 但当前没有任何可用于 Nginx 的 IP/CIDR 时,后端会拒绝同步,避免误写 `deny all` 把 Gitea 锁死。
- 远端路径要求绝对路径,拒绝 `/``/etc``/etc/nginx` 和控制字符。
## 自动同步触发点
开启 `gitea_allowlist_auto_sync=true` 后:
1. `POST /api/settings/ip-allowlist` 保存 Nexus 白名单后同步;
2. `POST /api/settings/ip-allowlist/manual` 增加手动 IP 后同步;
3. `DELETE /api/settings/ip-allowlist/ip` 删除手动 IP 后同步;
4. 后台订阅刷新 `server/background/ip_allowlist_refresh.py` 成功更新 `login_allowed_ips` 后同步。
自动同步是 best-effort:失败只记录 `gitea_allowlist_last_sync_error`,不会影响 Nexus 原本白名单保存。
## 前端页面
新增 `/app-v2` 设置页中的 `Gitea 白名单同步` 面板:
- 开关:启用 Gitea 白名单、自动同步;
- 输入:Gitea 服务器 ID、include 远端路径、reload 命令;
- 展示:Nexus 合并白名单数量、Nginx 可用 IP/CIDR 数量、跳过项数量;
- 展示:Nginx include 示例和即将写入文件预览;
- 操作:保存配置、立即同步。
## 回滚
如果需要回滚:
1. 在 Nexus 关闭 `Gitea 白名单启用`
2. 点击“立即同步”,生成不拦截的 include;
3. 或从 Gitea Nginx 配置中移除 `include /etc/nginx/gitea-allowlist.conf;`
4. 执行 `nginx -t && systemctl reload nginx`
@@ -0,0 +1,175 @@
# Nexus SSH/文件管理安全巡检阶段 12026-07-07
## 1. 巡检口径调整
用户已明确:`script_service` 是“管理员远程执行命令”功能,天然允许管理员执行任意 shell;这是产品设计内能力,不作为本轮漏洞修复项。
后续对它只保留三类审查:
- 鉴权:必须只能由已登录管理员调用。
- 审计:执行人、目标服务器、执行时间、结果摘要要可追踪。
- 敏感信息:不要把密码、Token、密钥等完整写入日志或返回给不该看到的人。
本轮实际修复的是“非脚本服务、非预期的命令参数边界”。
---
## 2. 当前测试机状态
- 测试机:`192.168.124.219`
- 项目目录:`/opt/nexus-dev-current`
- Git 分支:`audit/security-review`
- 当前最新提交:`bcec78f fix: harden file manager shell operations`
- Nexus 容器:`0.0.0.0:18600 -> 8600`,健康检查返回 `ok`
- MySQL`0.0.0.0:13306 -> 3306`
- Redis`0.0.0.0:16379 -> 6379`
最近提交:
```text
bcec78f fix: harden file manager shell operations
112e617 fix: stop archive option injection via filenames
99edd30 chore: default compose ports for nexus test deploy
a538358 fix: keep bt panel sessions alive after one-click login
9d42fcd chore: baseline Nexus LAN test deployment snapshot
```
---
## 3. 已完成修复 A:远程压缩命令参数终止符
提交:`112e617 fix: stop archive option injection via filenames`
修改文件:
- `server/infrastructure/ssh/remote_archive.py`
- `tests/test_remote_archive_commands.py`
修复点:
- `tar` 压缩命令增加 `--`,避免成员名以 `-` 开头时被当作 tar 参数。
- `zip` 压缩命令增加 `--`,避免目标名或成员名以 `-` 开头时被当作 zip 参数。
运行容器已热更新确认包含:
```text
tar -czf ... -C ... -- ...
zip -qr -- ...
```
---
## 4. 已完成修复 B:文件管理命令参数终止符
提交:`bcec78f fix: harden file manager shell operations`
修改文件:
- `server/api/sync_v2.py`
加固命令:
```text
rm -rf -- <path>
mv -- <src> <dest>
mkdir -p -- <path>
cp -r -- <sources> <dest>
mv -t <dest> -- <sources>
```
说明:
- 这些路径本身已经经过 `normalize_remote_abs_path()``shlex.quote()`
- 本次增加 `--` 是第二道防线,防止未来路径来源变化或边界输入导致工具把路径误判为命令选项。
- 这不是限制管理员能力;管理员确实要执行任意命令时仍应通过设计内的 `script_service`
---
## 5. 已完成修复 C:递归 chmod/chown 系统目录子树保护
提交:`bcec78f fix: harden file manager shell operations`
修改文件:
- `server/utils/files_chmod_policy.py`
- `tests/test_file_permissions.py`
修复前:
- 只禁止对精确系统路径递归改权限,例如 `/etc``/usr``/`
- 但 `/etc/nginx``/usr/local` 这类系统目录子树仍可能被文件管理 UI 执行递归 chmod/chown。
修复后:
- 继续禁止精确系统路径。
- 新增禁止关键系统目录子树:
- `/bin/`
- `/boot/`
- `/dev/`
- `/etc/`
- `/lib/`
- `/lib64/`
- `/proc/`
- `/run/`
- `/sbin/`
- `/sys/`
- `/usr/`
- `/var/cache/`
- `/var/lib/`
- `/var/log/`
- `/var/run/`
- `/var/spool/`
- 保留常见 Web 目录可操作:
- `/www/wwwroot/site`
- `/var/www/html`
设计原因:
- 文件管理 UI 的 chmod/chown 是“安全操作封装”,应防止误点造成系统不可用。
- 真正需要改系统目录权限的管理员,仍可使用设计内的 `script_service` 明确执行命令。
---
## 6. 验证结果
已运行测试:
```text
pytest tests/test_file_permissions.py tests/test_schema_path_validators.py tests/test_remote_archive_commands.py -q
22 passed in 0.54s
```
完整后端测试:
```text
pytest -q
733 passed, 1 skipped in 11.77s
```
格式检查:
```text
git diff --check
无输出,表示无 whitespace 错误。
```
运行容器热更新:
- 已把以下文件复制进 `nexus-nexus-1` 容器:
- `server/infrastructure/ssh/remote_archive.py`
- `server/api/sync_v2.py`
- `server/utils/files_chmod_policy.py`
- 已重启 Nexus 容器。
- 健康检查返回 `ok`
---
## 7. 下一步继续巡检建议
下一阶段建议按以下顺序继续:
1. `server/api/sync_v2.py` 解压链路:检查 tar/zip 解压是否需要增加归档成员路径校验,防 zip-slip/tar traversal。
2. `server/application/services/server_file_transfer_service.py`:检查跨服务器打包/投递的路径校验、临时目录清理、权限回退。
3. `server/infrastructure/ssh/remote_shell.py``asyncssh_pool.py`:复核 timeout、sudo fallback、stderr/stdout 截断和敏感信息处理。
4. `server/infrastructure/btpanel/ssh_bootstrap.py``ssh_login.py`:复核宝塔 token、白名单、session patch 输出中是否有敏感信息泄漏。
5. `server/api/servers.py`:复核 agent 安装/升级命令拼接、参数来源、失败回滚边界。
@@ -0,0 +1,177 @@
# Nexus SSH/文件管理安全巡检阶段 2:安全解压(2026-07-07
## 1. 本阶段目标
继续 SSH/文件管理安全巡检,重点检查:
- `/api/sync/decompress` 解压链路。
- tar/zip 归档成员是否可能写出目标目录。
- 归档中的软链接、硬链接、设备文件是否可能造成覆盖或越权写入。
`script_service` 仍按用户确认的口径处理:它是设计内管理员远程 shell,不作为漏洞;本阶段只加固文件管理 UI 的“封装式安全操作”。
---
## 2. 修复提交
提交:
```text
845d7e2 fix: validate remote archives before extraction
```
修改文件:
```text
server/api/sync_v2.py
server/infrastructure/ssh/remote_archive.py
tests/test_remote_archive_commands.py
```
---
## 3. 修复前风险
原解压逻辑直接执行:
```bash
tar xzf <archive> -C <dest>
unzip -o <archive> -d <dest>
```
虽然 `<archive>``<dest>` 已经做过绝对路径规范化和 shell quote,但归档文件内部成员名不受 Nexus 控制,仍需要额外检查:
- 成员名为 `../evil` 可能尝试路径穿越。
- 成员名为 `/etc/passwd` 属于绝对路径。
- zip 中 Windows 分隔符 `dir\evil` 在某些工具/平台语义下容易产生歧义。
- 归档内软链接/硬链接/设备文件可能造成解压后写出目标目录或制造特殊文件。
---
## 4. 修复后设计
解压流程从 API 中下沉到:
```text
server/infrastructure/ssh/remote_archive.py::run_remote_decompress()
```
新流程:
1. 判断格式:
- `.tar.gz` / `.tgz` -> `tar.gz`
- `.zip` -> `zip`
2. 先列出成员名:
- tar`tar -tzf <archive>`
- zip`unzip -Z1 <archive>`
3. 校验成员名:
- 禁止空文件名。
- 禁止绝对路径。
- 禁止 `..` 路径穿越。
- 禁止 Windows 反斜杠分隔符。
4. 再列出成员类型:
- tar`tar -tvzf <archive>`
- zip`zipinfo -l <archive>`
5. 校验成员类型:
- 允许普通文件和目录。
- 禁止符号链接。
- 禁止硬链接。
- 禁止块设备、字符设备、FIFO、socket。
6. 通过校验后才解压。
安全 tar 解压命令增加:
```bash
tar --no-same-owner --no-same-permissions -xzf <archive> -C <dest>
```
避免归档强行恢复 owner/permission,降低解压后权限异常风险。
---
## 5. 新增测试覆盖
新增/扩展测试文件:
```text
tests/test_remote_archive_commands.py
```
覆盖:
- tar/zip 成员列表命令。
- tar 解压命令使用 `--no-same-owner --no-same-permissions`
- 拒绝 `../evil.txt`
- 拒绝 `/etc/passwd`
- 拒绝 `dir\evil.txt`
- 允许普通相对文件和目录。
- 拒绝符号链接。
- 拒绝字符设备。
- 允许 zipinfo header/footer + 普通文件/目录。
---
## 6. 验证结果
相关测试:
```text
pytest tests/test_remote_archive_commands.py tests/test_schema_path_validators.py tests/test_file_permissions.py -q
31 passed in 0.55s
```
完整后端测试:
```text
pytest -q
742 passed, 1 skipped in 11.83s
```
格式检查:
```text
git diff --check
无输出
```
运行容器热更新:
- 已复制以下文件到 `nexus-nexus-1`
- `server/api/sync_v2.py`
- `server/infrastructure/ssh/remote_archive.py`
- 已重启 Nexus 容器。
- `http://127.0.0.1:18600/health` 返回 `ok`
- 容器内已确认存在:
- `validate_archive_member_names`
- `run_remote_decompress`
- API 调用 `run_remote_decompress(...)`
---
## 7. 当前最新提交链
```text
845d7e2 fix: validate remote archives before extraction
bcec78f fix: harden file manager shell operations
112e617 fix: stop archive option injection via filenames
99edd30 chore: default compose ports for nexus test deploy
a538358 fix: keep bt panel sessions alive after one-click login
```
---
## 8. 下一阶段建议
继续巡检:
1. `server/application/services/server_file_transfer_service.py`
- 跨服务器打包/拉取/投递路径校验。
- 临时文件清理。
- sudo fallback 边界。
2. `server/infrastructure/ssh/remote_shell.py` / `asyncssh_pool.py`
- timeout 默认值。
- stdout/stderr 截断。
- 敏感信息是否可能返回前端。
3. `server/api/servers.py`
- agent 安装/升级/回滚命令拼接。
- 参数来源和 quote。
@@ -0,0 +1,87 @@
# Nexus SSH 命令执行安全巡检阶段 3:跨服务器文件传输与项目专用 Skill
日期:2026-07-07
分支:`audit/security-review`
范围:跨服务器文件传输、BT 面板直链下载投递、远程归档压缩/解压、项目专用审查 Skill。
## 1. 本阶段结论
本阶段补齐了跨服务器文件传输链路中遗留的高风险边界:
- 传输包在目标服务器解压时,复用了统一的安全解压流程,先列成员、校验路径和类型,再解压。
- 远程 `mkdir``mv``rm`、归档 staging move 增加 `--` 参数终止符,避免以 `-` 开头的路径被当成命令选项。
- `pull_transfer_package` 与 BT 直链投递目标路径改为统一 `normalize_remote_abs_path` 校验,拒绝 `..` 与反斜杠。
- 新增 `.skills/` 下 Nexus 专用安全巡检规范,后续每轮审查可按固定规则执行。
完整测试结果:`748 passed, 1 skipped`
## 2. 调用链
### 2.1 Nexus 代理下载投递
```mermaid
flowchart TD
A["API: transfer package/deliver"] --> B["server_file_transfer_service.create_transfer_package"]
B --> C["run_remote_compress(source)"]
C --> D["Redis transfer_package_store: token/meta/ttl"]
D --> E["pull_transfer_package(dest)"]
E --> F["curl Nexus signed transfer URL to /tmp staging"]
F --> G["mv -- staging -> dest archive path"]
G --> H["run_remote_decompress(dest): list members"]
H --> I["validate names/types"]
I --> J["safe tar/unzip extract"]
J --> K["apply_transfer_permissions"]
```
### 2.2 BT 面板直链下载投递
```mermaid
flowchart TD
A["deliver_transfer_package"] --> B["source has BT API credentials"]
B --> C["_deliver_via_btpanel_download"]
C --> D["run_remote_compress(source)"]
D --> E["signed_download_url(BT)"]
E --> F["dest curl BT download URL to /tmp staging"]
F --> G["mv -- staging -> dest archive path"]
G --> H["run_remote_decompress(dest)"]
H --> I["validate archive member names/types"]
I --> J["extract + permissions"]
```
## 3. 修复点
| 文件 | 修复 |
|---|---|
| `server/application/services/server_file_transfer_service.py` | `_extract_archive_on_server()` 改为调用 `run_remote_decompress()`,解压前校验归档成员;目标路径使用 `normalize_remote_abs_path()``mkdir/mv/rm` 增加 `--`。 |
| `server/infrastructure/ssh/remote_archive.py` | tar/zip staging 压缩后的 `mv` 改为 `mv -f -- ...`。 |
| `tests/test_server_file_transfer.py` | 新增安全解压委托、解压校验错误、目标路径穿越拒绝、下载 staging move/rm 参数终止符测试;更新旧的 tar overwrite 行为测试。 |
| `tests/test_remote_archive_commands.py` | 新增 tar/zip staging move 参数终止符测试。 |
| `.skills/nexus-security-review/SKILL.md` | 固化 Nexus 安全巡检总规则。 |
| `.skills/nexus-ssh-safety/SKILL.md` | 固化 SSH 命令、路径、归档安全规则。 |
| `.skills/nexus-btpanel-review/SKILL.md` | 固化宝塔一键登录/凭据/会话修复审查规则。 |
## 4. 边界说明
- `script_service` 是管理员显式远程 shell 能力,本阶段没有把它作为命令注入漏洞处理;后续只审查鉴权、审计、超时、输出截断和脱敏。
- 文件传输仍允许管理员选择任意绝对目标路径,但不允许路径字符串中包含 `..`、反斜杠或非绝对路径。
- 归档解压会拒绝绝对路径、父目录穿越、Windows 反斜杠、符号链接、硬链接和设备/FIFO/socket 等特殊条目。
- 未在报告中记录任何密码、token、cookie、SSH key 或 BT API key。
## 5. 验证记录
```bash
.venv/bin/python -m pytest tests/test_server_file_transfer.py tests/test_remote_archive_commands.py -q
# 40 passed
.venv/bin/python -m pytest -q
# 748 passed, 1 skipped
git diff --check
# pass
```
## 6. 下一步建议
1. 继续审查 `server/api/servers.py` 的 agent 安装/升级/回滚命令拼接与超时边界。
2. 审查 `remote_shell.py` / `asyncssh_pool.py` 的 stdout/stderr 截断、敏感输出脱敏和 sudo fallback 一致性。
3. 审查 API 鉴权覆盖:确认所有管理接口均有管理员依赖,公开下载类接口只暴露必要的 token 校验。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,236 @@
# Nexus 手动发布命令清单(2026-07-08)
> 适用场景:Codex 远程 SSH 审批不可用时,由人工在本机和线上服务器执行。
> 发布包已通过本地 Gate 和发布包扫描。
## 本地发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 方案 A:使用现有 deploy/deploy-production.sh(推荐)
前提:
- 本机能通过 SSH 访问线上 Host:`nexus`
- 或设置 `NEXUS_SSH=user@host`
- 线上已有 `/opt/nexus``/www/wwwroot/api.synaglobal.vip`
在源码快照目录执行:
```bash
cd /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/work/nexus_code_context/snapshots/nexus-source-20260707-112042
bash deploy/deploy-production.sh
```
如果不用 SSH config
```bash
export NEXUS_SSH='root@你的线上服务器IP'
export NEXUS_DEPLOY_PATH='/opt/nexus'
bash deploy/deploy-production.sh
```
发布脚本会:
1. 本地同步源码到线上。
2. 排除 `.venv-*``node_modules`、缓存、误生成文件。
3. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`
4. 构建并上传旧 `/app/`
5. 构建并上传新 `/app-v2/`
6. Docker 模式下同步 `web/app``web/app-v2` 到容器。
7. 检查 `/health``/app/``/app-v2/`
## 方案 B:发布 tar.gz 包到线上
### 1. 上传
Windows CMD / PowerShell
```cmd
scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz
```
### 2. 线上校验
```bash
cd /tmp
sha256sum nexus-release-20260708.tar.gz
```
必须等于:
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 3. 线上备份
假设部署目录为 `/opt/nexus`
```bash
sudo mkdir -p /opt/nexus-backups
sudo tar czf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt nexus
```
### 3B. 使用服务器侧脚本自动应用发布包(推荐)
如果线上已存在当前项目的 `deploy/apply-release-bundle.sh`,或者你先把发布包解开到临时目录后使用新脚本,可以执行:
```bash
sudo bash /opt/nexus/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
如果脚本尚未在旧线上目录中,可以先临时解出脚本:
```bash
cd /tmp
tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh
sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
这个脚本会自动:
1. 校验 SHA256。
2. 检查发布包内必须路径和禁止路径。
3. 备份当前 `/opt/nexus`
4. rsync 覆盖代码,但保留 `.env``docker/.env.prod``web/data`
5. Docker 模式下执行 `nexus-1panel.sh upgrade --skip-git`
6. 同步 `/app/``/app-v2/` 静态资源到容器。
7. 验证本机 `/health``/app/``/app-v2/`
### 4. 解包覆盖
```bash
sudo mkdir -p /opt/nexus
sudo tar xzf /tmp/nexus-release-20260708.tar.gz -C /tmp
sudo rsync -a --delete \
--exclude='.env' \
--exclude='docker/.env.prod' \
--exclude='web/data' \
/tmp/nexus-release/ /opt/nexus/
```
### 5. 重建/重启
Docker/1Panel 模式:
```bash
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git
```
如果是 Supervisor 模式:
```bash
sudo supervisorctl restart nexus
```
### 6. Docker 容器内静态资源同步
Docker 模式建议执行:
```bash
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/sync_webapp_to_container.sh
```
## 方案 C:Python 发布辅助脚本(推荐替代手工 scp/ssh)
默认只上传并执行服务器侧 preflight,不会正式 apply
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
preflight 输出确认无误后,再加 `--apply` 正式发布:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
详见:`Nexus-release-python-publisher-20260708.md`
## 发布后验证
本机执行:
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
线上执行:
```bash
curl -s https://api.synaglobal.vip/health
curl -I https://api.synaglobal.vip/app/
curl -I https://api.synaglobal.vip/app-v2/
```
期望:
```text
/health => ok
/app/ => HTTP 200
/app-v2/ => HTTP 200
```
## 宝塔专项验证
发布后在 Nexus 后台验证:
1. 打开 `/app/` 旧后台,确认宝塔列表正常。
2. 打开 `/app-v2/` 新后台,确认页面可访问。
3. 对同一台宝塔连续点击一键登录,冲突时应该返回 HTTP 409。
4. 对新增宝塔执行一键登录,应触发 hardcoded_3600 检测/修复兜底。
5. 多台宝塔同时打开后,2 小时刷新不应因为 `task.py` hardcoded `3600` 陆续掉线。
## 回滚
如果异常:
```bash
cd /opt
sudo mv nexus nexus-bad-20260708
sudo tar xzf /opt/nexus-backups/nexus-before-20260708.tar.gz -C /opt
cd /opt/nexus
sudo env NEXUS_ROOT=/opt/nexus bash deploy/nexus-1panel.sh upgrade --skip-git
```
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.
@@ -0,0 +1,21 @@
# Nexus release artifact metadata
- File: `nexus-release-20260708.tar.gz`
- Size: `27.02 MB`
- Entries: `2409`
- SHA256: `46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b`
- Created UTC: `2026-07-08T09:59:20.278597+00:00`
## Verify after upload
Linux:
```bash
sha256sum nexus-release-20260708.tar.gz
```
PowerShell:
```powershell
Get-FileHash .\nexus-release-20260708.tar.gz -Algorithm SHA256
```
@@ -0,0 +1,777 @@
# Nexus release diff manifest (2026-07-08)
Baseline:
- Old snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236`
- New snapshot: `C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042`
Excluded names/patterns: `.git`, `node_modules`, `.venv-*`, `venv`, `__pycache__`, `.ruff_cache`, `.playwright-mcp`, `.cursor`, `.pytest_cache`, `tmp`, `*.pyc`, `*.pyo`, `*.bak-`, `2025.2`, `=2025.2`.
## Summary
- Added files: 381
- Modified files: 26
- Deleted files: 347
## Added files
- `.skills/nexus-btpanel-review/SKILL.md`
- `.skills/nexus-security-review/SKILL.md`
- `.skills/nexus-ssh-safety/SKILL.md`
- `deploy/apply-release-bundle.sh`
- `deploy/preflight-release-host.sh`
- `docs/security/Nexus-SSH鍛戒护鎵ц瀹夊叏宸℃闃舵3-鏂囦欢浼犺緭涓庝笓鐢⊿kill-20260707.md`
- `frontend-v2/index.html`
- `frontend-v2/package-lock.json`
- `frontend-v2/package.json`
- `frontend-v2/postcss.config.js`
- `frontend-v2/scripts/scan-app-v2-sensitive.mjs`
- `frontend-v2/src/App.tsx`
- `frontend-v2/src/components/ui/Badge.tsx`
- `frontend-v2/src/components/ui/Panel.tsx`
- `frontend-v2/src/features/dashboard/DashboardPage.tsx`
- `frontend-v2/src/features/dashboard/api/dashboardApi.ts`
- `frontend-v2/src/features/dashboard/components/AppShell.tsx`
- `frontend-v2/src/features/dashboard/components/AssetInventoryPage.tsx`
- `frontend-v2/src/features/dashboard/components/AuditLogPage.tsx`
- `frontend-v2/src/features/dashboard/components/AuditTimeline.tsx`
- `frontend-v2/src/features/dashboard/components/BtDomainSslPage.tsx`
- `frontend-v2/src/features/dashboard/components/BtPanelPage.tsx`
- `frontend-v2/src/features/dashboard/components/BtResourceOverviewPage.tsx`
- `frontend-v2/src/features/dashboard/components/CommandPalette.tsx`
- `frontend-v2/src/features/dashboard/components/ExecutionRecordsPage.tsx`
- `frontend-v2/src/features/dashboard/components/GlobalSearchPage.tsx`
- `frontend-v2/src/features/dashboard/components/HealthScore.tsx`
- `frontend-v2/src/features/dashboard/components/OperationLogsPage.tsx`
- `frontend-v2/src/features/dashboard/components/ReadOnlyFileBrowser.tsx`
- `frontend-v2/src/features/dashboard/components/ScriptLibraryPage.tsx`
- `frontend-v2/src/features/dashboard/components/SecurityInspectionPage.tsx`
- `frontend-v2/src/features/dashboard/components/ServerDrawer.tsx`
- `frontend-v2/src/features/dashboard/components/ServerTable.tsx`
- `frontend-v2/src/features/dashboard/components/SettingsOverviewPage.tsx`
- `frontend-v2/src/features/dashboard/components/StatCard.tsx`
- `frontend-v2/src/features/dashboard/components/TaskCenter.tsx`
- `frontend-v2/src/features/dashboard/components/TaskCenterPage.tsx`
- `frontend-v2/src/features/dashboard/components/TerminalFilesPage.tsx`
- `frontend-v2/src/features/dashboard/components/TrendPanel.tsx`
- `frontend-v2/src/features/dashboard/components/TtlMatrix.tsx`
- `frontend-v2/src/features/dashboard/components/WatchMonitoringPage.tsx`
- `frontend-v2/src/features/dashboard/data/dashboardData.ts`
- `frontend-v2/src/features/dashboard/hooks/useDashboardData.ts`
- `frontend-v2/src/lib/api.ts`
- `frontend-v2/src/lib/redaction.ts`
- `frontend-v2/src/lib/utils.ts`
- `frontend-v2/src/main.tsx`
- `frontend-v2/src/stores/appStore.ts`
- `frontend-v2/src/styles.css`
- `frontend-v2/src/vite-env.d.ts`
- `frontend-v2/tailwind.config.ts`
- `frontend-v2/tsconfig.json`
- `frontend-v2/tsconfig.node.json`
- `frontend-v2/vite.config.ts`
- `scripts/check_backend_test_env.py`
- `scripts/check_deploy_excludes.py`
- `scripts/check_text_integrity.py`
- `scripts/generate_release_bundle.py`
- `scripts/generate_release_diff.py`
- `scripts/prepare_release_artifact.py`
- `scripts/publish_release_bundle.py`
- `scripts/run_btpanel_tests.py`
- `scripts/run_btpanel_tests_docker.cmd`
- `scripts/run_btpanel_tests_docker.sh`
- `scripts/run_local_release_gate.py`
- `scripts/scan_release_bundle.py`
- `scripts/scan_release_manifest.py`
- `scripts/verify_online_release.py`
- `scripts/write_release_metadata.py`
- `tests/test_btpanel_bootstrap_lock.py`
- `tests/test_publish_release_bundle.py`
- `tests/test_remote_archive_commands.py`
- `web/app-v2/assets/DashboardPage--9cH96CJ.js`
- `web/app-v2/assets/index-8f2_PJZM.js`
- `web/app-v2/assets/index-saKc1OPG.css`
- `web/app-v2/assets/query-B5fiFlYp.js`
- `web/app-v2/assets/react-DGbf-iXy.js`
- `web/app-v2/assets/rolldown-runtime-Bh1tDfsg.js`
- `web/app-v2/assets/table-C-Cg70AL.js`
- `web/app-v2/assets/ui-sq2aW9r5.js`
- `web/app-v2/index.html`
- `web/app/assets/AlertsPage-BZa6JbW1.js`
- `web/app/assets/AlertsPage-DkbuUWgZ.js`
- `web/app/assets/AuditPage-CrzMBMPz.js`
- `web/app/assets/AuditPage-DE5F-bot.js`
- `web/app/assets/BtPanelCrontabPage-CsxFXzwR.js`
- `web/app/assets/BtPanelCrontabPage-DFAuKeNI.js`
- `web/app/assets/BtPanelDatabasesPage-Cua9aENZ.js`
- `web/app/assets/BtPanelDatabasesPage-Cxt_i4Lp.js`
- `web/app/assets/BtPanelDomainsPage-9oKLFhi4.js`
- `web/app/assets/BtPanelDomainsPage-CSupPzQH.js`
- `web/app/assets/BtPanelLoginPage-DiKTnvyN.js`
- `web/app/assets/BtPanelLoginPage-Dp3X10Er.js`
- `web/app/assets/BtPanelMonitorPage-2tHIkE0-.js`
- `web/app/assets/BtPanelMonitorPage-Pm3fw_df.js`
- `web/app/assets/BtPanelMonitorPage-Td-5siPa.css`
- `web/app/assets/BtPanelServicesPage-B652HAqe.js`
- `web/app/assets/BtPanelServicesPage-DR8K2l-F.js`
- `web/app/assets/BtPanelSettingsPage-BBi6ttQm.js`
- `web/app/assets/BtPanelSettingsPage-DCQKy6JG.js`
- `web/app/assets/BtPanelSiteCreatePage-DLG_BM-C.js`
- `web/app/assets/BtPanelSiteCreatePage-DTCrnEa5.js`
- `web/app/assets/BtPanelSitesPage--EP6G5x3.js`
- `web/app/assets/BtPanelSitesPage-DWm4LMaV.js`
- `web/app/assets/BtPanelSslPage-BuiQZ1OJ.js`
- `web/app/assets/BtPanelSslPage-JWzPztxi.js`
- `web/app/assets/CommandsPage-BNtXfIOs.js`
- `web/app/assets/CommandsPage-CuHLCAHz.js`
- `web/app/assets/CredentialsPage-BPUBEwg9.js`
- `web/app/assets/CredentialsPage-Dc92himy.js`
- `web/app/assets/DashboardPage-DOc8f-2E.js`
- `web/app/assets/DashboardPage-DmG38yGr.css`
- `web/app/assets/DashboardPage-yElaw2BV.js`
- `web/app/assets/FilesPage-Be2kmggp.js`
- `web/app/assets/FilesPage-By07WOZR.css`
- `web/app/assets/FilesPage-DHwnC_As.js`
- `web/app/assets/FleetTrendEChart-DbHCuhg7.css`
- `web/app/assets/FleetTrendEChart-Dz57W1gD.js`
- `web/app/assets/FleetTrendEChart-lgdTP3R-.js`
- `web/app/assets/LoginPage-3NZZLudg.js`
- `web/app/assets/LoginPage-BKIQ3yge.js`
- `web/app/assets/LoginPage-BTEVFKpe.css`
- `web/app/assets/PushPage-BcIAXgGU.js`
- `web/app/assets/PushPage-DnVPUcAX.css`
- `web/app/assets/PushPage-_afVGSg2.js`
- `web/app/assets/RetriesPage-BdJWTxtp.js`
- `web/app/assets/RetriesPage-C70vf571.js`
- `web/app/assets/SchedulesPage-B47mlwc8.js`
- `web/app/assets/SchedulesPage-B9PFTR71.css`
- `web/app/assets/SchedulesPage-DM0j6jps.js`
- `web/app/assets/ScriptRunsPage-CW2C9c7C.css`
- `web/app/assets/ScriptRunsPage-DIeeXpt5.js`
- `web/app/assets/ScriptRunsPage-DeZ4sRVP.js`
- `web/app/assets/ScriptsPage-BWoaZhKf.js`
- `web/app/assets/ScriptsPage-BrIbZLc5.css`
- `web/app/assets/ScriptsPage-D8An6uCU.js`
- `web/app/assets/ServerCategoryPicker-BJm8eTKs.js`
- `web/app/assets/ServerCategoryPicker-BlyQtEug.js`
- `web/app/assets/ServerCategoryPicker-CHn3BA4z.css`
- `web/app/assets/ServerQuickAddDialogs-CoSabmGi.js`
- `web/app/assets/ServerQuickAddDialogs-zcLgLArY.js`
- `web/app/assets/ServerTransferPage-BdwpJEc4.css`
- `web/app/assets/ServerTransferPage-CWGjaCXR.js`
- `web/app/assets/ServerTransferPage-UYjJ6gzQ.js`
- `web/app/assets/ServersPage-CEOh0Y1q.css`
- `web/app/assets/ServersPage-D7wE_7hM.css`
- `web/app/assets/ServersPage-nR5-sjDp.js`
- `web/app/assets/ServersPage-nhvNw2_Z.js`
- `web/app/assets/SettingsPage-Am37wCvf.js`
- `web/app/assets/SettingsPage-FAHRRqSI.js`
- `web/app/assets/StatCardsRow-CWt_wcFU.js`
- `web/app/assets/StatCardsRow-CdfBB9tI.js`
- `web/app/assets/StatCardsRow-snFLCRnD.css`
- `web/app/assets/TerminalPage-CNkogNbG.js`
- `web/app/assets/TerminalPage-DX9qLtN2.js`
- `web/app/assets/TerminalPage-DvGyoWff.css`
- `web/app/assets/VAutocomplete-B1NSIBuv.js`
- `web/app/assets/VAutocomplete-CZMI3cz9.js`
- `web/app/assets/VAutocomplete-D48FgPMA.css`
- `web/app/assets/VCard-BCYbpv-w.css`
- `web/app/assets/VCard-BwwttBcf.js`
- `web/app/assets/VCard-pmpQR9O7.js`
- `web/app/assets/VCheckbox-BO-mWtTk.js`
- `web/app/assets/VCheckbox-ChRKi1kj.css`
- `web/app/assets/VCheckbox-RnLQ4WBr.js`
- `web/app/assets/VDataTable-ClfE0-sD.js`
- `web/app/assets/VDataTable-D61zAwkI.css`
- `web/app/assets/VDataTable-pRRodit6.js`
- `web/app/assets/VDataTableServer-BIMNqMvk.js`
- `web/app/assets/VDataTableServer-BysXbZ8p.js`
- `web/app/assets/VDialog-BFQGuuXw.js`
- `web/app/assets/VDialog-DP-kc9Gx.css`
- `web/app/assets/VDialog-nKJcjfNL.js`
- `web/app/assets/VExpansionPanels-ByOIBgKv.js`
- `web/app/assets/VExpansionPanels-CUfe7Zz5.css`
- `web/app/assets/VExpansionPanels-DUlWPWNJ.js`
- `web/app/assets/VForm-CAvEgM-c.js`
- `web/app/assets/VForm-uEqKaHHF.js`
- `web/app/assets/VRow-DIr3-SqU.css`
- `web/app/assets/VRow-qjxp2VJ5.js`
- `web/app/assets/VRow-rmSa_q-C.js`
- `web/app/assets/VSelect-3h-EI23Y.js`
- `web/app/assets/VSelect-BmvHN1-i.css`
- `web/app/assets/VSelect-BvR-yyLS.js`
- `web/app/assets/VSkeletonLoader-BW_kZu-c.js`
- `web/app/assets/VSkeletonLoader-CkAIsbz1.css`
- `web/app/assets/VSkeletonLoader-gL4kF19X.js`
- `web/app/assets/VSwitch-CIruBPax.js`
- `web/app/assets/VSwitch-DPhHiAlh.css`
- `web/app/assets/VSwitch-VDVIZTO7.js`
- `web/app/assets/VTable-BTftprIQ.js`
- `web/app/assets/VTable-Dq_7MYoa.css`
- `web/app/assets/VTable-w1sxrmvd.js`
- `web/app/assets/VTabs-BX9iYxVa.js`
- `web/app/assets/VTabs-Bq6bCiCC.js`
- `web/app/assets/VTabs-DjRHpLZ2.css`
- `web/app/assets/VTextField-B-BnrJAQ.js`
- `web/app/assets/VTextField-Bl91kSVs.css`
- `web/app/assets/VTextField-GUQr1tUN.js`
- `web/app/assets/VTextarea-BMTWx02W.js`
- `web/app/assets/VTextarea-DDl63AeD.css`
- `web/app/assets/VTextarea-PzNEhaMZ.js`
- `web/app/assets/VTooltip-BbRT1OE8.js`
- `web/app/assets/VTooltip-aiKJ1MVA.css`
- `web/app/assets/VTooltip-tVMR683k.js`
- `web/app/assets/WatchMetricsPage-BjqkPE11.css`
- `web/app/assets/WatchMetricsPage-CL8abV00.js`
- `web/app/assets/WatchMetricsPage-DByjAcyM.css`
- `web/app/assets/WatchMetricsPage-DY4qY84I.js`
- `web/app/assets/_plugin-vue_export-helper-BDNMzG2s.js`
- `web/app/assets/abap-08VXUWAP.js`
- `web/app/assets/apex-BWPQTe0t.js`
- `web/app/assets/apiError-BUh3zCmq.js`
- `web/app/assets/apiError-CpgZRt4T.js`
- `web/app/assets/auditNormalize-xrm2MK_4.js`
- `web/app/assets/azcli-Bc_sGQ0U.js`
- `web/app/assets/bat-i0X4ZdIN.js`
- `web/app/assets/bicep-B5-_aFwp.js`
- `web/app/assets/btpanel-BsL25ffd.js`
- `web/app/assets/btpanel-C_41A_fE.js`
- `web/app/assets/cameligo-DMUM7wLl.js`
- `web/app/assets/chunk-DK3Fl9T5.js`
- `web/app/assets/clojure-Cm7r79vr.js`
- `web/app/assets/codicon-ngg6Pgfi.ttf`
- `web/app/assets/coffee-Ba7i2nA0.js`
- `web/app/assets/cpp-C7h46wYY.js`
- `web/app/assets/csharp-BKxtCVv1.js`
- `web/app/assets/csp-bTuwJoIa.js`
- `web/app/assets/css-DIMkf-bt.js`
- `web/app/assets/css.worker-CvXBzhp8.js`
- `web/app/assets/cssMode-CnBxfcpP.js`
- `web/app/assets/cssMode-DoYviH0F.js`
- `web/app/assets/cypher-CVaqCwHa.js`
- `web/app/assets/dart-onAF5SnQ.js`
- `web/app/assets/dataTable-esDY4cOk.js`
- `web/app/assets/dist-DCycUQQT.js`
- `web/app/assets/dist-DMqKEX4A.js`
- `web/app/assets/dockerfile-DZFCIeNp.js`
- `web/app/assets/ecl-D05T4iGw.js`
- `web/app/assets/editor.worker-Cn2oRESe.js`
- `web/app/assets/elixir-6RTg0lbw.js`
- `web/app/assets/execDetailGrouping-B4lJ15VJ.js`
- `web/app/assets/execDetailGrouping-CuMwXaZZ.js`
- `web/app/assets/flow9-C5_-GSwl.js`
- `web/app/assets/freemarker2-BUYL0LB-.js`
- `web/app/assets/freemarker2-D0e9LerR.js`
- `web/app/assets/fsharp-C8Ef5oNN.js`
- `web/app/assets/go-C-y9NEjX.js`
- `web/app/assets/graphql-fmXr3nnJ.js`
- `web/app/assets/handlebars-C8vPzJIb.js`
- `web/app/assets/handlebars-ncsxj4cu.js`
- `web/app/assets/hcl-CpzslTdj.js`
- `web/app/assets/html-8-zTuU_5.js`
- `web/app/assets/html-DUT2IBNz.js`
- `web/app/assets/html.worker-BO6WuOEO.js`
- `web/app/assets/htmlMode-Bm_X9rOD.js`
- `web/app/assets/htmlMode-C8LJuzCB.js`
- `web/app/assets/index-BNJe_ogF.css`
- `web/app/assets/index-CDcgx9V8.js`
- `web/app/assets/index-a0fi5f-0.js`
- `web/app/assets/ini-sBoK_t0W.js`
- `web/app/assets/initMonaco-Cjv05FdX.js`
- `web/app/assets/initMonaco-DTDMLv7Z.js`
- `web/app/assets/initMonaco-DX2BVZF3.css`
- `web/app/assets/install-BY4V-NK4.js`
- `web/app/assets/install-bdNoH-9Q.js`
- `web/app/assets/java-BEtHBSE6.js`
- `web/app/assets/javascript-B4NqljlL.js`
- `web/app/assets/javascript-wfMVVC84.js`
- `web/app/assets/json.worker-BkJRGcCJ.js`
- `web/app/assets/jsonMode-B06-mTjs.js`
- `web/app/assets/jsonMode-B7uld_NH.js`
- `web/app/assets/julia-Bri6UV-V.js`
- `web/app/assets/kotlin-BOotOW0E.js`
- `web/app/assets/labels-C-A2d08W.js`
- `web/app/assets/less-B9JPFI3C.js`
- `web/app/assets/lexon-CfSJPG6W.js`
- `web/app/assets/liquid-B1dfVIHc.js`
- `web/app/assets/liquid-BxkVsnPx.js`
- `web/app/assets/lspLanguageFeatures-3WI5pdzZ.js`
- `web/app/assets/lspLanguageFeatures-CaMGOy4a.js`
- `web/app/assets/lua-CsQS60Ue.js`
- `web/app/assets/m3-D-oSqn_W.js`
- `web/app/assets/markdown-Cimd5fb3.js`
- `web/app/assets/materialdesignicons-webfont-B7mPwVP_.ttf`
- `web/app/assets/materialdesignicons-webfont-CSr8KVlo.eot`
- `web/app/assets/materialdesignicons-webfont-Dp5v-WZN.woff2`
- `web/app/assets/materialdesignicons-webfont-PXm3-2wK.woff`
- `web/app/assets/mdx-B6lamGpH.js`
- `web/app/assets/mdx-lREt6kXe.js`
- `web/app/assets/mips-CIPQ_RoX.js`
- `web/app/assets/msdax-DauUninz.js`
- `web/app/assets/mysql-SOo6toE5.js`
- `web/app/assets/objective-c-FvmIjYaQ.js`
- `web/app/assets/paginatedFetch-CijkqaDG.js`
- `web/app/assets/paginatedFetch-VYTwXwno.js`
- `web/app/assets/pascal-DrH0SRf2.js`
- `web/app/assets/pascaligo-D-ptJ9y-.js`
- `web/app/assets/perl-oz_6vUea.js`
- `web/app/assets/pgsql-DTj74zXo.js`
- `web/app/assets/php-nr791fC2.js`
- `web/app/assets/pla-CopQ2nXW.js`
- `web/app/assets/postiats-43DmfD33.js`
- `web/app/assets/powerquery-D3hlyOfw.js`
- `web/app/assets/powershell-DmHpPYUd.js`
- `web/app/assets/protobuf-C531GsRP.js`
- `web/app/assets/pug-Z5eAx3Zn.js`
- `web/app/assets/python-Dt0lyPW-.js`
- `web/app/assets/python-DwgZPXhL.js`
- `web/app/assets/qsharp-DkqhCAOL.js`
- `web/app/assets/r-BwWrilGY.js`
- `web/app/assets/razor-B1t_g3Ke.js`
- `web/app/assets/razor-Dv9uiya3.js`
- `web/app/assets/redis-ClamHrr6.js`
- `web/app/assets/redshift-DT7zqm-g.js`
- `web/app/assets/remotePath-D3rG2Z3e.js`
- `web/app/assets/restructuredtext-BYgofb2h.js`
- `web/app/assets/roboto-latin-300-normal-BARJ-h6h.woff`
- `web/app/assets/roboto-latin-300-normal-CCzlftfr.woff2`
- `web/app/assets/roboto-latin-400-normal-BqEyEoaF.woff2`
- `web/app/assets/roboto-latin-400-normal-DyYNIH4P.woff`
- `web/app/assets/roboto-latin-500-normal-7RbcRiD8.woff2`
- `web/app/assets/roboto-latin-500-normal-DQZyH_nt.woff`
- `web/app/assets/roboto-latin-700-normal-BZpUvMxY.woff2`
- `web/app/assets/roboto-latin-700-normal-DLgJJpmK.woff`
- `web/app/assets/ruby-DezsRK8O.js`
- `web/app/assets/rust-DdL9SqIa.js`
- `web/app/assets/sb-CcwsVR0C.js`
- `web/app/assets/scala-DHpiXF5c.js`
- `web/app/assets/scheme-BeGwcela.js`
- `web/app/assets/scss-gp-XZpBa.js`
- `web/app/assets/serverSiteUrl-DDZRgSNA.js`
- `web/app/assets/shell-CC2rA5mh.js`
- `web/app/assets/solidity-BEEn4gHE.js`
- `web/app/assets/sophia-CRfGWb83.js`
- `web/app/assets/sparql-D_Lu-MrJ.js`
- `web/app/assets/sql-NEE52Syq.js`
- `web/app/assets/st-DbInun42.js`
- `web/app/assets/status-BrGQYcd-.js`
- `web/app/assets/status-XDgjPQrL.js`
- `web/app/assets/swift-Bxkupp3x.js`
- `web/app/assets/systemverilog-Bz4Y3fRF.js`
- `web/app/assets/tcl-DISqw1ZD.js`
- `web/app/assets/ts.worker-B6r0Skfj.js`
- `web/app/assets/tsMode-9bXRGx5g.js`
- `web/app/assets/tsMode-ChdzhlZm.js`
- `web/app/assets/twig-De2hgUGE.js`
- `web/app/assets/typescript-D3T7er6k.js`
- `web/app/assets/typescript-DRN2FRNg.js`
- `web/app/assets/typespec-B8J7ngcE.js`
- `web/app/assets/useBtPanelLogin-DCNB-U_J.js`
- `web/app/assets/useBtPanelLogin-DDcleXPj.js`
- `web/app/assets/useBtPanelPageLoad-CD81wbTP.js`
- `web/app/assets/useBtPanelPageLoad-CV_unS2-.js`
- `web/app/assets/usePageActivateRefresh-BntaOABK.js`
- `web/app/assets/usePageActivateRefresh-CqHSpagO.js`
- `web/app/assets/usePageAutoRefresh-DW8DyVQQ.js`
- `web/app/assets/usePageAutoRefresh-DvG6Fd0_.js`
- `web/app/assets/usePushStaging-BCIq1TPK.js`
- `web/app/assets/usePushStaging-CfCU7s8k.css`
- `web/app/assets/usePushStaging-DcgeHeSX.js`
- `web/app/assets/useServerCategories-BcvoZF0H.js`
- `web/app/assets/useServerCategories-V7luFEnW.js`
- `web/app/assets/useServerList-6fsbQxWS.js`
- `web/app/assets/useServerList-w3j3GRMG.js`
- `web/app/assets/useServerSearchHistory-BnDuA-7p.js`
- `web/app/assets/useServerSearchHistory-C3ha-zP2.css`
- `web/app/assets/useServerSearchHistory-CTnIHvTd.js`
- `web/app/assets/useSnackbar-u_XK4ZNm.js`
- `web/app/assets/useTerminalQuickCommands-CGHoPHjs.js`
- `web/app/assets/useTerminalQuickCommands-DKqOac6p.js`
- `web/app/assets/useTerminalQuickCommands-DyOcH0CY.css`
- `web/app/assets/useWatchPins-DTUw7j9v.js`
- `web/app/assets/useWatchPins-DxD91kwD.js`
- `web/app/assets/validation-BMzYTJL4.js`
- `web/app/assets/vb-DV3o63ZY.js`
- `web/app/assets/wgsl-DpFanUEy.js`
- `web/app/assets/xml-BFdUA9kH.js`
- `web/app/assets/xml-Btmx4yuJ.js`
- `web/app/assets/yaml-DKn-1xmA.js`
- `web/app/assets/yaml-tUm_O8N4.js`
## Modified files
- `.dockerignore`
- `Dockerfile`
- `Dockerfile.prod`
- `deploy/deploy-production.sh`
- `deploy/rsync-local-to-server.sh`
- `deploy/sync_webapp_to_container.sh`
- `docker-compose.yml`
- `docs/research/jumpserver-easynode-architecture-analysis.md`
- `requirements-dev.txt`
- `server/api/assets.py`
- `server/api/scripts.py`
- `server/api/sync_v2.py`
- `server/application/services/btpanel_service.py`
- `server/application/services/server_file_transfer_service.py`
- `server/infrastructure/btpanel/bootstrap_lock.py`
- `server/infrastructure/btpanel/login_url_lock.py`
- `server/infrastructure/btpanel/ssh_bootstrap.py`
- `server/infrastructure/ssh/remote_archive.py`
- `server/main.py`
- `server/utils/files_chmod_policy.py`
- `tests/test_btpanel_get_config.py`
- `tests/test_btpanel_login_url.py`
- `tests/test_btpanel_login_url_route.py`
- `tests/test_btpanel_ssh_bootstrap.py`
- `tests/test_file_permissions.py`
- `tests/test_server_file_transfer.py`
## Deleted files
- `docker/.env.example`
- `docker/.env.prod.example`
- `docker/profiles/1c4g.env`
- `docker/profiles/2c8g.env`
- `docker/profiles/4c16g.env`
- `docs/Nexus-功能说明书.html`
- `frontend/.env.development`
- `scripts/npm-proxy.env`
- `web/app/dist/assets/AlertsPage-ji5eXKTc.js`
- `web/app/dist/assets/AuditPage-C3fmqt6f.js`
- `web/app/dist/assets/CommandsPage-C6QrgHcR.js`
- `web/app/dist/assets/CredentialsPage-oLbFLgd2.js`
- `web/app/dist/assets/DashboardPage-CKfPpNOT.js`
- `web/app/dist/assets/FilesPage-BvG082ru.css`
- `web/app/dist/assets/FilesPage-DNKIBjNs.js`
- `web/app/dist/assets/LoginPage-D8ZqP1P_.js`
- `web/app/dist/assets/MonacoEditor-Sfe2OjtA.js`
- `web/app/dist/assets/PushPage-BDdmIVXZ.js`
- `web/app/dist/assets/PushPage-ewjA8E7l.css`
- `web/app/dist/assets/RetriesPage-BJbJspGS.js`
- `web/app/dist/assets/SchedulesPage-D6oD8ext.js`
- `web/app/dist/assets/ScriptsPage-CbZT-hk0.js`
- `web/app/dist/assets/ServersPage-B4elP3Z1.js`
- `web/app/dist/assets/ServersPage-DjRHpLZ2.css`
- `web/app/dist/assets/SettingsPage-BuPIwZWo.js`
- `web/app/dist/assets/TerminalPage-BGKczTgd.js`
- `web/app/dist/assets/TerminalPage-Ol9WjDlo.css`
- `web/app/dist/assets/VAlert-B33BooF0.css`
- `web/app/dist/assets/VAlert-DGGVwUFF.js`
- `web/app/dist/assets/VDataTable-0Xurhrxv.js`
- `web/app/dist/assets/VDataTable-DlpDfCoY.css`
- `web/app/dist/assets/VDataTableServer-FvIQviSN.js`
- `web/app/dist/assets/VDialog-BLibPcoM.js`
- `web/app/dist/assets/VDialog-DP-kc9Gx.css`
- `web/app/dist/assets/VFileInput-C6sFcwsM.js`
- `web/app/dist/assets/VFileInput-HYy7i9zh.css`
- `web/app/dist/assets/VRow-DIr3-SqU.css`
- `web/app/dist/assets/VRow-r8nkWmtD.js`
- `web/app/dist/assets/VSelect-ALpjlDrA.css`
- `web/app/dist/assets/VSelect-Bz5bHvpa.js`
- `web/app/dist/assets/VSelectionControl-BTVr3HQm.css`
- `web/app/dist/assets/VSelectionControl-C1Y9bNOF.js`
- `web/app/dist/assets/VSpacer-DWZdSXUK.js`
- `web/app/dist/assets/VSpacer-DfbUir7X.css`
- `web/app/dist/assets/VSwitch-DPhHiAlh.css`
- `web/app/dist/assets/VSwitch-DY1GlILd.js`
- `web/app/dist/assets/VTextarea-3TieuoAv.js`
- `web/app/dist/assets/VTextarea-DDl63AeD.css`
- `web/app/dist/assets/abap-08VXUWAP.js`
- `web/app/dist/assets/apex-BWPQTe0t.js`
- `web/app/dist/assets/azcli-Bc_sGQ0U.js`
- `web/app/dist/assets/bat-i0X4ZdIN.js`
- `web/app/dist/assets/bicep-B5-_aFwp.js`
- `web/app/dist/assets/cameligo-DMUM7wLl.js`
- `web/app/dist/assets/clojure-Cm7r79vr.js`
- `web/app/dist/assets/codicon-ngg6Pgfi.ttf`
- `web/app/dist/assets/coffee-Ba7i2nA0.js`
- `web/app/dist/assets/cpp-C7h46wYY.js`
- `web/app/dist/assets/csharp-BKxtCVv1.js`
- `web/app/dist/assets/csp-bTuwJoIa.js`
- `web/app/dist/assets/css-DIMkf-bt.js`
- `web/app/dist/assets/css.worker-CvXBzhp8.js`
- `web/app/dist/assets/cssMode-DMvENJ_w.js`
- `web/app/dist/assets/cypher-CVaqCwHa.js`
- `web/app/dist/assets/dart-onAF5SnQ.js`
- `web/app/dist/assets/dockerfile-DZFCIeNp.js`
- `web/app/dist/assets/ecl-D05T4iGw.js`
- `web/app/dist/assets/editor-DX2BVZF3.css`
- `web/app/dist/assets/editor.api2-MWf-eAi-.js`
- `web/app/dist/assets/elixir-6RTg0lbw.js`
- `web/app/dist/assets/flow9-C5_-GSwl.js`
- `web/app/dist/assets/freemarker2-DQGAilFk.js`
- `web/app/dist/assets/fsharp-C8Ef5oNN.js`
- `web/app/dist/assets/go-C-y9NEjX.js`
- `web/app/dist/assets/graphql-fmXr3nnJ.js`
- `web/app/dist/assets/handlebars-DCagTjhi.js`
- `web/app/dist/assets/hcl-CpzslTdj.js`
- `web/app/dist/assets/html-nrfydv4h.js`
- `web/app/dist/assets/html.worker-BO6WuOEO.js`
- `web/app/dist/assets/htmlMode-Bp4WNwyQ.js`
- `web/app/dist/assets/index-DOFNruam.js`
- `web/app/dist/assets/index-YClpuO23.css`
- `web/app/dist/assets/ini-sBoK_t0W.js`
- `web/app/dist/assets/java-BEtHBSE6.js`
- `web/app/dist/assets/javascript-B0RcszDa.js`
- `web/app/dist/assets/json.worker-BkJRGcCJ.js`
- `web/app/dist/assets/jsonMode-D0vdA3eU.js`
- `web/app/dist/assets/julia-Bri6UV-V.js`
- `web/app/dist/assets/kotlin-BOotOW0E.js`
- `web/app/dist/assets/less-B9JPFI3C.js`
- `web/app/dist/assets/lexon-CfSJPG6W.js`
- `web/app/dist/assets/liquid-DAtIwle-.js`
- `web/app/dist/assets/lspLanguageFeatures-DRDm87pn.js`
- `web/app/dist/assets/lua-CsQS60Ue.js`
- `web/app/dist/assets/m3-D-oSqn_W.js`
- `web/app/dist/assets/markdown-Cimd5fb3.js`
- `web/app/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf`
- `web/app/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot`
- `web/app/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2`
- `web/app/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff`
- `web/app/dist/assets/mdx-BvtNR28y.js`
- `web/app/dist/assets/mips-CIPQ_RoX.js`
- `web/app/dist/assets/monaco.contribution-fyH2rvpO.js`
- `web/app/dist/assets/msdax-DauUninz.js`
- `web/app/dist/assets/mysql-SOo6toE5.js`
- `web/app/dist/assets/objective-c-FvmIjYaQ.js`
- `web/app/dist/assets/pascal-DrH0SRf2.js`
- `web/app/dist/assets/pascaligo-D-ptJ9y-.js`
- `web/app/dist/assets/perl-oz_6vUea.js`
- `web/app/dist/assets/pgsql-DTj74zXo.js`
- `web/app/dist/assets/php-nr791fC2.js`
- `web/app/dist/assets/pla-CopQ2nXW.js`
- `web/app/dist/assets/postiats-43DmfD33.js`
- `web/app/dist/assets/powerquery-D3hlyOfw.js`
- `web/app/dist/assets/powershell-DmHpPYUd.js`
- `web/app/dist/assets/protobuf-C531GsRP.js`
- `web/app/dist/assets/pug-Z5eAx3Zn.js`
- `web/app/dist/assets/python-Y2lQRCrb.js`
- `web/app/dist/assets/qsharp-DkqhCAOL.js`
- `web/app/dist/assets/r-BwWrilGY.js`
- `web/app/dist/assets/razor-DHI-mrmN.js`
- `web/app/dist/assets/redis-ClamHrr6.js`
- `web/app/dist/assets/redshift-DT7zqm-g.js`
- `web/app/dist/assets/restructuredtext-BYgofb2h.js`
- `web/app/dist/assets/roboto-cyrillic-100-italic-CNP2SmvR.woff2`
- `web/app/dist/assets/roboto-cyrillic-100-italic-GfU4zM_J.woff`
- `web/app/dist/assets/roboto-cyrillic-100-normal-D_oR061d.woff2`
- `web/app/dist/assets/roboto-cyrillic-100-normal-w5umKD67.woff`
- `web/app/dist/assets/roboto-cyrillic-300-italic-Bbg0zipm.woff2`
- `web/app/dist/assets/roboto-cyrillic-300-italic-BfviqnaP.woff`
- `web/app/dist/assets/roboto-cyrillic-300-normal-DEFNdjk5.woff`
- `web/app/dist/assets/roboto-cyrillic-300-normal-DzUz0kzv.woff2`
- `web/app/dist/assets/roboto-cyrillic-400-italic-BSQCxleC.woff`
- `web/app/dist/assets/roboto-cyrillic-400-italic-CqC_ywG3.woff2`
- `web/app/dist/assets/roboto-cyrillic-400-normal-Bjg-1-sg.woff`
- `web/app/dist/assets/roboto-cyrillic-400-normal-CBPI_iaY.woff2`
- `web/app/dist/assets/roboto-cyrillic-500-italic-CFOZTHyM.woff2`
- `web/app/dist/assets/roboto-cyrillic-500-italic-UnqpRRY_.woff`
- `web/app/dist/assets/roboto-cyrillic-500-normal-CBKMylY4.woff`
- `web/app/dist/assets/roboto-cyrillic-500-normal-CLao9AfR.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-italic-DhNJyfCT.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-italic-K45OuYTL.woff`
- `web/app/dist/assets/roboto-cyrillic-700-normal-C2o7G-SM.woff2`
- `web/app/dist/assets/roboto-cyrillic-700-normal-DhZFXDSN.woff`
- `web/app/dist/assets/roboto-cyrillic-900-italic-CW7hKEUp.woff2`
- `web/app/dist/assets/roboto-cyrillic-900-italic-Dw8LBt_T.woff`
- `web/app/dist/assets/roboto-cyrillic-900-normal-B-XH5ueX.woff`
- `web/app/dist/assets/roboto-cyrillic-900-normal-BVOxCBIE.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-CHVV4rI0.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-100-italic-Drj7gMwC.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-mbO7vZh1.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-100-normal-uxSc4Dbo.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-D2iEI_X7.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-300-italic-OnrzXwVD.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-D7ank4TF.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-300-normal-DIxttMbC.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-4ESj9BbU.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-400-italic-MZ-G6OiW.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-CaK1767H.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-400-normal-qHufge6k.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-C-yqETei.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-italic-ExZ7-vA1.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-BWC_xYeb.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-500-normal-DqF2hftb.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-BPGsVrTY.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-700-italic-DEF-7i1Y.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-CI7FH63F.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-700-normal-DmFxo5wj.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-CSUk7NP9.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-900-italic-acKlpJv6.woff2`
- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-Dy18Zgm9.woff`
- `web/app/dist/assets/roboto-cyrillic-ext-900-normal-IZ2B0aiV.woff2`
- `web/app/dist/assets/roboto-greek-100-italic-BByoHCxw.woff2`
- `web/app/dist/assets/roboto-greek-100-italic-Cy3jdfDA.woff`
- `web/app/dist/assets/roboto-greek-100-normal-DgpMWfbq.woff2`
- `web/app/dist/assets/roboto-greek-100-normal-ZuTz319d.woff`
- `web/app/dist/assets/roboto-greek-300-italic-BlmQgaZh.woff`
- `web/app/dist/assets/roboto-greek-300-italic-CznjWsFz.woff2`
- `web/app/dist/assets/roboto-greek-300-normal-C_Dgaih9.woff`
- `web/app/dist/assets/roboto-greek-300-normal-DJEM9B4Z.woff2`
- `web/app/dist/assets/roboto-greek-400-italic-BUWCAFe1.woff`
- `web/app/dist/assets/roboto-greek-400-italic-DnTUMkdF.woff2`
- `web/app/dist/assets/roboto-greek-400-normal-Bb5mj_fZ.woff`
- `web/app/dist/assets/roboto-greek-400-normal-ai2Z1K3C.woff2`
- `web/app/dist/assets/roboto-greek-500-italic-Dqz20l7v.woff2`
- `web/app/dist/assets/roboto-greek-500-italic-xdwGU0k_.woff`
- `web/app/dist/assets/roboto-greek-500-normal-C9AnhcmC.woff2`
- `web/app/dist/assets/roboto-greek-500-normal-oCqhoyfc.woff`
- `web/app/dist/assets/roboto-greek-700-italic-DawR0hnC.woff2`
- `web/app/dist/assets/roboto-greek-700-italic-P6YHZuSy.woff`
- `web/app/dist/assets/roboto-greek-700-normal-0aHWxGLu.woff2`
- `web/app/dist/assets/roboto-greek-700-normal-DjRqqLBV.woff`
- `web/app/dist/assets/roboto-greek-900-italic-B7xCHAj9.woff`
- `web/app/dist/assets/roboto-greek-900-italic-DvS3zuic.woff2`
- `web/app/dist/assets/roboto-greek-900-normal-B5AAzeOC.woff2`
- `web/app/dist/assets/roboto-greek-900-normal-DWdVoZCP.woff`
- `web/app/dist/assets/roboto-latin-100-italic-CMH2tK0H.woff2`
- `web/app/dist/assets/roboto-latin-100-italic-DZWoXrp_.woff`
- `web/app/dist/assets/roboto-latin-100-normal-CGMktwvD.woff`
- `web/app/dist/assets/roboto-latin-100-normal-vTzS_GaG.woff2`
- `web/app/dist/assets/roboto-latin-300-italic-B5i8yzYq.woff`
- `web/app/dist/assets/roboto-latin-300-italic-zCpkrxSM.woff2`
- `web/app/dist/assets/roboto-latin-300-normal-BARJ-h6h.woff`
- `web/app/dist/assets/roboto-latin-300-normal-CCzlftfr.woff2`
- `web/app/dist/assets/roboto-latin-400-italic-C2a9rKC1.woff`
- `web/app/dist/assets/roboto-latin-400-italic-CSuqwwKr.woff2`
- `web/app/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2`
- `web/app/dist/assets/roboto-latin-400-normal-DyYNIH4P.woff`
- `web/app/dist/assets/roboto-latin-500-italic-Bzx6Emny.woff`
- `web/app/dist/assets/roboto-latin-500-italic-DOe3GFcv.woff2`
- `web/app/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2`
- `web/app/dist/assets/roboto-latin-500-normal-DQZyH_nt.woff`
- `web/app/dist/assets/roboto-latin-700-italic-CC4lh_E9.woff`
- `web/app/dist/assets/roboto-latin-700-italic-Cb9agdGy.woff2`
- `web/app/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2`
- `web/app/dist/assets/roboto-latin-700-normal-DLgJJpmK.woff`
- `web/app/dist/assets/roboto-latin-900-italic-BENCS7Df.woff2`
- `web/app/dist/assets/roboto-latin-900-italic-DNjw4RGV.woff`
- `web/app/dist/assets/roboto-latin-900-normal-F72S18P8.woff`
- `web/app/dist/assets/roboto-latin-900-normal-lk0O8k6m.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-italic-BzoOrAFP.woff`
- `web/app/dist/assets/roboto-latin-ext-100-italic-CprbSwJf.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-normal-Bue4UH9m.woff2`
- `web/app/dist/assets/roboto-latin-ext-100-normal-CRE1JcN2.woff`
- `web/app/dist/assets/roboto-latin-ext-300-italic-BaJRnD9g.woff`
- `web/app/dist/assets/roboto-latin-ext-300-italic-Bavh33sq.woff2`
- `web/app/dist/assets/roboto-latin-ext-300-normal-B90pq-BC.woff2`
- `web/app/dist/assets/roboto-latin-ext-300-normal-CTCCHkZF.woff`
- `web/app/dist/assets/roboto-latin-ext-400-italic-BilqUbDw.woff2`
- `web/app/dist/assets/roboto-latin-ext-400-italic-g8iNtijM.woff`
- `web/app/dist/assets/roboto-latin-ext-400-normal-C3tdtHj3.woff2`
- `web/app/dist/assets/roboto-latin-ext-400-normal-scX0fKtV.woff`
- `web/app/dist/assets/roboto-latin-ext-500-italic-B5teBpxj.woff2`
- `web/app/dist/assets/roboto-latin-ext-500-italic-BlZHPPdA.woff`
- `web/app/dist/assets/roboto-latin-ext-500-normal-Cyc0AKLz.woff`
- `web/app/dist/assets/roboto-latin-ext-500-normal-pMCM9Ixg.woff2`
- `web/app/dist/assets/roboto-latin-ext-700-italic-C8tIie4u.woff`
- `web/app/dist/assets/roboto-latin-ext-700-italic-CBdFfgzf.woff2`
- `web/app/dist/assets/roboto-latin-ext-700-normal-BUhwtWwy.woff`
- `web/app/dist/assets/roboto-latin-ext-700-normal-DSBUz0N1.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-italic-CFogYjYt.woff`
- `web/app/dist/assets/roboto-latin-ext-900-italic-CgK4zYaI.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-normal-Bg1HnWxG.woff2`
- `web/app/dist/assets/roboto-latin-ext-900-normal-CUx1IrMY.woff`
- `web/app/dist/assets/roboto-math-100-italic-B4vNMY8j.woff`
- `web/app/dist/assets/roboto-math-100-italic-C4gy5Bp3.woff2`
- `web/app/dist/assets/roboto-math-100-normal-3u4f34A3.woff2`
- `web/app/dist/assets/roboto-math-100-normal-DzbsQ8e_.woff`
- `web/app/dist/assets/roboto-math-300-italic-CIRUIJB7.woff`
- `web/app/dist/assets/roboto-math-300-italic-XxAa5nOs.woff2`
- `web/app/dist/assets/roboto-math-300-normal-5dF_7mZP.woff2`
- `web/app/dist/assets/roboto-math-300-normal-Ds0YpBw2.woff`
- `web/app/dist/assets/roboto-math-400-italic-BpybG2ZH.woff2`
- `web/app/dist/assets/roboto-math-400-italic-CXOsqNh0.woff`
- `web/app/dist/assets/roboto-math-400-normal-BEFej5gc.woff2`
- `web/app/dist/assets/roboto-math-400-normal-C9RxBKAh.woff`
- `web/app/dist/assets/roboto-math-500-italic-BaDHGWdF.woff2`
- `web/app/dist/assets/roboto-math-500-italic-Dr-Gj3Mh.woff`
- `web/app/dist/assets/roboto-math-500-normal-C-7mKPO3.woff`
- `web/app/dist/assets/roboto-math-500-normal-C4NU9gLX.woff2`
- `web/app/dist/assets/roboto-math-700-italic-B5uBDPR4.woff`
- `web/app/dist/assets/roboto-math-700-italic-BTlAqkAP.woff2`
- `web/app/dist/assets/roboto-math-700-normal-B8YqGHVc.woff2`
- `web/app/dist/assets/roboto-math-700-normal-DVoD5t2k.woff`
- `web/app/dist/assets/roboto-math-900-italic-B-PPazk1.woff`
- `web/app/dist/assets/roboto-math-900-italic-D-7BUet6.woff2`
- `web/app/dist/assets/roboto-math-900-normal-DQ66ivDi.woff2`
- `web/app/dist/assets/roboto-math-900-normal-Dmeiz_CW.woff`
- `web/app/dist/assets/roboto-symbols-100-italic-DSe_C1Iy.woff`
- `web/app/dist/assets/roboto-symbols-100-italic-DkwGdnY5.woff2`
- `web/app/dist/assets/roboto-symbols-100-normal-CjKjWFkd.woff2`
- `web/app/dist/assets/roboto-symbols-100-normal-CqmTeVyX.woff`
- `web/app/dist/assets/roboto-symbols-300-italic-BfpS6Q35.woff2`
- `web/app/dist/assets/roboto-symbols-300-italic-Dm46KJRd.woff`
- `web/app/dist/assets/roboto-symbols-300-normal-BCnjhQd_.woff`
- `web/app/dist/assets/roboto-symbols-300-normal-DDU7avhj.woff2`
- `web/app/dist/assets/roboto-symbols-400-italic-CQIwDYNA.woff`
- `web/app/dist/assets/roboto-symbols-400-italic-DJOHuWBY.woff2`
- `web/app/dist/assets/roboto-symbols-400-normal-CB1Ce4Gk.woff2`
- `web/app/dist/assets/roboto-symbols-400-normal-DLYbZahX.woff`
- `web/app/dist/assets/roboto-symbols-500-italic-BiGHMB5Q.woff2`
- `web/app/dist/assets/roboto-symbols-500-italic-Dz3aAlfh.woff`
- `web/app/dist/assets/roboto-symbols-500-normal-B_CZKVJS.woff2`
- `web/app/dist/assets/roboto-symbols-500-normal-F7c8nfcH.woff`
- `web/app/dist/assets/roboto-symbols-700-italic-Bj79QYOr.woff2`
- `web/app/dist/assets/roboto-symbols-700-italic-Dm-8FOP3.woff`
- `web/app/dist/assets/roboto-symbols-700-normal-BiFDindJ.woff2`
- `web/app/dist/assets/roboto-symbols-700-normal-BoS6HWkc.woff`
- `web/app/dist/assets/roboto-symbols-900-italic-BZDs0BY5.woff`
- `web/app/dist/assets/roboto-symbols-900-italic-QOKtSfbw.woff2`
- `web/app/dist/assets/roboto-symbols-900-normal-1vlkxR2C.woff2`
- `web/app/dist/assets/roboto-symbols-900-normal-9XmQV1ku.woff`
- `web/app/dist/assets/roboto-vietnamese-100-italic-BnSYtt9K.woff2`
- `web/app/dist/assets/roboto-vietnamese-100-italic-Hu6kAu-e.woff`
- `web/app/dist/assets/roboto-vietnamese-100-normal-Cc5a3-TP.woff`
- `web/app/dist/assets/roboto-vietnamese-100-normal-KgOkQYnu.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-italic-B9qvi8_k.woff`
- `web/app/dist/assets/roboto-vietnamese-300-italic-CtUKlc4H.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-normal-BPvXm_f1.woff2`
- `web/app/dist/assets/roboto-vietnamese-300-normal-INUupD3o.woff`
- `web/app/dist/assets/roboto-vietnamese-400-italic-C41J4i52.woff2`
- `web/app/dist/assets/roboto-vietnamese-400-italic-RF0eew8q.woff`
- `web/app/dist/assets/roboto-vietnamese-400-normal-D2PTxGxD.woff2`
- `web/app/dist/assets/roboto-vietnamese-400-normal-DnpnVwnf.woff`
- `web/app/dist/assets/roboto-vietnamese-500-italic-BddoBFd0.woff2`
- `web/app/dist/assets/roboto-vietnamese-500-italic-CmEfQ1B1.woff`
- `web/app/dist/assets/roboto-vietnamese-500-normal-B3ncpOoB.woff2`
- `web/app/dist/assets/roboto-vietnamese-500-normal-D380IkQ8.woff`
- `web/app/dist/assets/roboto-vietnamese-700-italic-By4b4cXv.woff`
- `web/app/dist/assets/roboto-vietnamese-700-italic-CVc74C74.woff2`
- `web/app/dist/assets/roboto-vietnamese-700-normal-BEVeWqJt.woff2`
- `web/app/dist/assets/roboto-vietnamese-700-normal-DsFyXAL4.woff`
- `web/app/dist/assets/roboto-vietnamese-900-italic-D0nMCc5h.woff`
- `web/app/dist/assets/roboto-vietnamese-900-italic-hVMOC7we.woff2`
- `web/app/dist/assets/roboto-vietnamese-900-normal-C48YQOjq.woff`
- `web/app/dist/assets/roboto-vietnamese-900-normal-D-H2ldSl.woff2`
- `web/app/dist/assets/ruby-DezsRK8O.js`
- `web/app/dist/assets/rust-DdL9SqIa.js`
- `web/app/dist/assets/sb-CcwsVR0C.js`
- `web/app/dist/assets/scala-DHpiXF5c.js`
- `web/app/dist/assets/scheme-BeGwcela.js`
- `web/app/dist/assets/scss-gp-XZpBa.js`
- `web/app/dist/assets/shell-CC2rA5mh.js`
- `web/app/dist/assets/solidity-BEEn4gHE.js`
- `web/app/dist/assets/sophia-CRfGWb83.js`
- `web/app/dist/assets/sparql-D_Lu-MrJ.js`
- `web/app/dist/assets/sql-NEE52Syq.js`
- `web/app/dist/assets/st-DbInun42.js`
- `web/app/dist/assets/status-CNSle5LJ.js`
- `web/app/dist/assets/swift-Bxkupp3x.js`
- `web/app/dist/assets/systemverilog-Bz4Y3fRF.js`
- `web/app/dist/assets/tcl-DISqw1ZD.js`
- `web/app/dist/assets/ts.worker-B6r0Skfj.js`
- `web/app/dist/assets/tsMode-B1_NfjHa.js`
- `web/app/dist/assets/twig-De2hgUGE.js`
- `web/app/dist/assets/typescript-B9Bb_0iF.js`
- `web/app/dist/assets/typespec-B8J7ngcE.js`
- `web/app/dist/assets/useServerList-CokRfazm.js`
- `web/app/dist/assets/useSnackbar-D7YOfKok.js`
- `web/app/dist/assets/validation-Yxuom3Du.js`
- `web/app/dist/assets/vb-DV3o63ZY.js`
- `web/app/dist/assets/wgsl-DpFanUEy.js`
- `web/app/dist/assets/workers-DLQCZ--h.js`
- `web/app/dist/assets/xml-BsJ5i10T.js`
- `web/app/dist/assets/yaml-DT_Rfd72.js`
- `web/app/dist/favicon.ico`
- `web/app/dist/index.html`
@@ -0,0 +1,148 @@
# Nexus Release Index — 2026-07-08
## Status
```text
local_artifact_ready=true
online_deployed=false
remote_ssh_blocked_by_codex_approval=true
```
Codex has not modified the production server in this phase. Remote SSH/network checks were blocked by the approval auto-review endpoint returning HTTP 404.
## Artifact
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256:
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
Standard checksum file:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256
```
Metadata:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md
```
Current artifact facts:
```text
size_mb=27.02
entries=2409
files=2409
```
## Main documents
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-ready-checklist-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-app-v2-双后台实施说明-20260707.md
```
## One-command local preparation
Run from the source snapshot:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708
```
Verified result:
```text
[prepare-release-artifact] OK
added=381 modified=26 deleted=347
60 passed in 1.70s
OK: release bundle looks clean
Validate-only mode: tarball checksum and content validation passed
```
## Server-side apply command
Upload:
```cmd
scp C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz nexus:/tmp/nexus-release-20260708.tar.gz
```
If the new apply script is not yet on the server, extract it first:
```bash
cd /tmp
tar xzf nexus-release-20260708.tar.gz nexus-release/deploy/apply-release-bundle.sh
```
Validate only:
```bash
bash /tmp/nexus-release/deploy/apply-release-bundle.sh --validate-only \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
Apply:
```bash
sudo bash /tmp/nexus-release/deploy/apply-release-bundle.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
## Online verification after deploy
```cmd
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
Expected:
```text
OK: health
OK: /app
OK: /app-v2
OK: online release verification passed
```
## BtPanel verification
After deploy:
1. Existing `/app/` works.
2. New `/app-v2/` works.
3. BtPanel one-click login works.
4. Same server duplicate one-click login is serialized; conflict returns HTTP 409.
5. Newly added BtPanel servers get hardcoded_3600 detection/repair before one-click login.
6. Multiple BtPanel sessions remain logged in after 2 hours and refresh.
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.
- `Nexus-release-text-integrity-gate-20260708.md`:本轮新增文本完整性 Gate、文档控制字符修复和最新制品 SHA 记录。
- `Nexus-release-python-publisher-20260708.md`:Python 版发布辅助脚本说明,避免 PowerShell 管道/引号/编码问题。
- `Nexus-release-publisher-tests-20260708.md`:发布辅助脚本离线测试覆盖与 diff manifest 排除 tmp 修复记录。
@@ -0,0 +1,55 @@
# Nexus 发布辅助脚本测试覆盖记录(2026-07-08)
## 本轮目的
继续降低线上发布风险:为 `scripts/publish_release_bundle.py` 增加离线单元测试,并修复 diff manifest 对测试临时文件的误收录。
## 新增测试
文件:
```text
work\nexus_code_context\snapshots\nexus-source-20260707-112042\tests\test_publish_release_bundle.py
```
覆盖 5 个场景:
1. 默认模式:上传 + preflight,不执行 apply
2. 显式 `--apply`preflight 后才执行 `apply-release-bundle.sh`
3. `--no-upload --dry-run`:只生成远端 SSH 命令,不触发 scp;
4. SHA256 mismatch:在任何远端命令前失败;
5. 远端路径:始终使用 POSIX `/tmp/...`,避免 Windows `\tmp\...`
## Gate 更新
`run_local_release_gate.py --with-pytest` 现在会执行:
- 宝塔专项测试:60 个;
- 发布辅助脚本测试:5 个。
合计:65 个 Python 测试。
## Manifest 修复
`scripts/generate_release_diff.py` 新增排除 `tmp` 目录,避免测试临时 tarball 进入 diff manifest。
已确认:
- release bundle 不包含 `tmp/pytest-publish-release`
- diff manifest 不包含 `tmp/pytest-publish-release`
- package scan 通过;
- validate-only 通过。
## 最新发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
entries`2409`
@@ -0,0 +1,81 @@
# Nexus Python 发布辅助脚本(2026-07-08
## 目的
为避免 Windows PowerShell 管道、编码和引号问题,新增 Python 发布辅助脚本:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042\scripts\publish_release_bundle.py
```
它使用 `subprocess` 参数列表调用 `scp` / `ssh`,不依赖本地 shell 管道;默认只上传发布包并执行服务器侧 preflight,不会直接发布。
## 最新发布包
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
## 推荐使用方式
### 1. 只上传并执行 preflight(默认安全模式)
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 2. 如果不使用 SSH config
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote root@你的线上服务器IP ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--deploy-path /opt/nexus
```
### 3. preflight 确认无误后执行 apply
```cmd
cd /d C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
.venv-py312-codex\Scripts\python.exe scripts\publish_release_bundle.py ^
--remote nexus ^
--tarball C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz ^
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b ^
--apply
```
## 行为说明
脚本会:
1. 本地校验 tarball SHA256
2. 使用 `scp` 上传到 `/tmp/nexus-release-20260708.tar.gz`
3. 在远端从发布包解出:
- `deploy/preflight-release-host.sh`
- `deploy/apply-release-bundle.sh`
4. 执行 preflight
5. 只有传入 `--apply` 时才执行正式 apply。
## 已验证
- `--help` 正常;
- `--dry-run --no-upload` 正常;
- Windows 下远端路径保持 POSIX `/tmp/...`,不会被转换成 `\tmp\...`
- 已纳入 `scripts/run_local_release_gate.py``py_compile`
## 本轮新增测试
- `tests/test_publish_release_bundle.py`:5 个离线测试,覆盖默认 preflight、显式 apply、SHA mismatch、no-upload dry-run、远端 POSIX 路径。
- 已纳入 `scripts/run_local_release_gate.py --with-pytest`
@@ -0,0 +1,495 @@
# Nexus 发布前清单(2026-07-08
## 结论
当前本地快照已经通过完整发布前 Gate:
```text
py_compile + /app-v2 build:safe + app-v2 sensitive scan + 宝塔 pytest
```
最终结果:
```text
[release-gate] OK
60 passed in 5.33s
```
## 当前源码快照
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042
```
对比基准快照:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236
```
完整差异清单:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
## 差异清单汇总
重新生成后的清单已经排除:
- `.git`
- `node_modules`
- `.venv-*`
- `venv`
- `__pycache__`
- `.ruff_cache`
- `.playwright-mcp`
- `.cursor`
- `.pytest_cache`
- `*.pyc`
- `*.pyo`
- `*.bak-`
- `2025.2`
- `=2025.2`
- `tmp/nexus-frontend.tar.gz`
当前差异统计:
```text
added=381
modified=26
deleted=347
```
注意:差异里包含大量旧 `/app/` 静态构建产物变动。如果只发布本轮宝塔稳定性与 `/app-v2`,可以优先发布“核心发布集合”,不要盲目同步整个快照目录。
## 核心发布集合
### 后端宝塔一键登录稳定性
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/main.py
```
### API / 服务相关历史改动
差异清单中还有以下后端文件发生变化,发布前需要按实际范围确认是否一起发布:
```text
server/api/assets.py
server/api/scripts.py
server/api/sync_v2.py
server/application/services/server_file_transfer_service.py
server/infrastructure/ssh/remote_archive.py
server/utils/files_chmod_policy.py
```
这些文件属于此前安全巡检/文件传输/脚本执行相关改动,不是本轮宝塔一键登录最小集合;如果线上已经发布过,可跳过或只按 diff 覆盖。
### `/app-v2` 双后台
源码:
```text
frontend-v2/
```
构建产物:
```text
web/app-v2/
```
后端静态挂载:
```text
server/main.py
```
Docker
```text
Dockerfile
Dockerfile.prod
```
### 测试与发布脚本
```text
requirements-dev.txt
scripts/check_backend_test_env.py
scripts/generate_release_diff.py
scripts/run_btpanel_tests.py
scripts/run_btpanel_tests_docker.cmd
scripts/run_btpanel_tests_docker.sh
scripts/run_local_release_gate.py
scripts/scan_release_manifest.py
```
### 测试文件
```text
tests/test_btpanel_bootstrap_lock.py
tests/test_btpanel_get_config.py
tests/test_btpanel_login_url.py
tests/test_btpanel_login_url_route.py
tests/test_btpanel_ssh_bootstrap.py
```
## 发布前验证命令
### 1. 重新生成差异清单
```cmd
.venv-py312-codex\Scripts\python.exe scripts\generate_release_diff.py --old C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --new C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
结果:
```text
added=369 modified=21 deleted=347
```
### 2. 扫描发布候选文件是否包含已知环境泄露标记
```cmd
.venv-py312-codex\Scripts\python.exe scripts\scan_release_manifest.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --manifest C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-diff-manifest-20260708.md
```
结果:
```text
OK: no known environment leak markers found in release manifest entries
```
### 3. 完整发布前 Gate
```cmd
.venv-py312-codex\Scripts\python.exe scripts\run_local_release_gate.py --with-pytest
```
结果:
```text
[app-v2-sensitive-scan] OK: scanned 9 file(s), no high-confidence secrets found
collected 60 items
60 passed in 1.79s
[release-gate] OK
```
最新 Gate 追加确认:
```text
bash -n deploy/sync_webapp_to_container.sh
60 passed in 1.70s
[release-gate] OK
```
### 4. 生成干净发布包
```cmd
.venv-py312-codex\Scripts\python.exe scripts\generate_release_bundle.py --root C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-112042 --output C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
结果:
```text
files=2409
```
发布包位置:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
发布包元数据:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.metadata.json
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-artifact-metadata-20260708.md
```
SHA256
```text
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
```
### 5. 扫描发布包
```cmd
.venv-py312-codex\Scripts\python.exe scripts\scan_release_bundle.py C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz
```
结果:
```text
size_mb=27.02
entries=2409
OK: release bundle looks clean
```
### 6. 部署排除规则自检
`scripts/run_local_release_gate.py` 现在会自动执行:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\check_deploy_excludes.py
```
结果:
```text
OK: deploy exclusion patterns are present
```
### 7. 发布后线上验证脚本
新增:
```text
scripts/verify_online_release.py
```
用途:
- 检查 `/health`
- 检查旧后台 `/app/`
- 检查新后台 `/app-v2/`
- 检查页面里不要出现 `全局 API Key``Global API Key``tmp_login``tmp_token`
发布后执行:
```cmd
.venv-py312-codex\Scripts\python.exe scripts\verify_online_release.py --base-url https://api.synaglobal.vip
```
## 明确不发布
以下内容不要同步到正式服务器:
```text
.venv-py312-codex/
.venv-py314/
venv/
frontend-v2/node_modules/
.pytest_cache/
__pycache__/
.ruff_cache/
.cursor/
.playwright-mcp/
frontend-v2/node_modules/
*.pyc
*.pyo
*.bak-
2025.2
=2025.2
tmp/nexus-frontend.tar.gz
```
其中 `2025.2` / `=2025.2` 是 Windows CMD 执行 `tzdata>=2025.2` 时可能产生的重定向误文件,绝对不要发布。
## 建议发布顺序
### 方案 A:Docker 镜像重建发布(推荐)
1. 使用当前快照作为构建上下文,但确保 `.dockerignore` 排除虚拟环境、node_modules、缓存和临时文件。
2. 构建生产镜像。
3. 启动新容器前备份旧容器镜像/tag 和数据库。
4. 发布后执行线上验证。
优点:静态资源、后端代码、依赖环境一致性更好。
当前已修复:
- `.dockerignore` 已排除 `frontend-v2/node_modules``.venv-*``.pytest_cache``2025.2``*.bak-` 等。
- `deploy/rsync-local-to-server.sh` 已排除 `frontend-v2/node_modules``.venv-*``.pytest_cache``2025.2``*.bak-` 等。
- `scripts/run_local_release_gate.py` 已增加 `bash -n` 检查 `deploy/deploy-production.sh``deploy/rsync-local-to-server.sh`
- `deploy/deploy-production.sh` 发布后健康检查已增加 `/app-v2/`
- `deploy/deploy-production.sh` 已显式构建并上传 `web/app-v2`,不只依赖 Dockerfile 构建。
- `deploy/apply-release-bundle.sh` 已新增,可在服务器侧对 tar.gz 发布包执行 SHA256 校验、备份、解包、重建和本机验证。
- `deploy/sync_webapp_to_container.sh` 已同步 `web/app-v2` 到容器,并校验 `/app-v2/` 主 bundle。
- `scripts/run_local_release_gate.py` 已增加 `bash -n deploy/sync_webapp_to_container.sh`
- `scripts/check_deploy_excludes.py` 会防止发布排除规则未来被误删。
### 当前远程执行状态
本轮尝试对线上 `Host nexus` 做只读 SSH 探测时,Codex 审批系统返回 404 并拒绝执行。不是 SSH 凭据或代码错误,而是自动审批代理不可用。
因此本轮没有实际修改线上服务器。当前状态是:
```text
本地发布包:已生成
发布包扫描:通过
完整 release gate:通过
线上部署:尚未执行
```
如果要由 Codex 继续实际发布,需要在下一步明确允许 SSH 发布操作。
## 手动发布命令清单
已生成:
```text
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-manual-deploy-commands-20260708.md
```
该文档包含:
- `deploy/deploy-production.sh` 一键发布方式。
- tar.gz 发布包上传方式。
- 线上 SHA256 校验。
- 线上备份、解包、Docker 重建。
- 发布后 `/health``/app/``/app-v2/` 验证。
- 宝塔专项验证。
- 回滚命令。
## 最新最终验证
发布包扫描:
```text
size_mb=27.02
entries=2409
OK: release bundle looks clean
```
完整 Gate
```text
60 passed in 1.70s
[release-gate] OK
```
### 方案 B:只同步核心文件到现有线上目录
适合快速热修:
1. 同步后端核心文件。
2. 同步 `web/app-v2/`
3. 重启后端服务。
4. 验证宝塔一键登录和 `/app-v2/`
缺点:如果线上依赖、静态目录或 Dockerfile 状态与本地不一致,容易遗漏。
## 线上验证清单
发布到:
```text
https://api.synaglobal.vip
```
后验证:
1. `/app/` 旧后台仍可访问。
2. `/app-v2/` 新后台可访问。
3. `/api/btpanel/servers/{id}/login-url` 正常返回登录地址。
4. 同一台宝塔短时间重复点一键登录,应该只允许一个签发流程,冲突时返回 HTTP 409。
5. 新增宝塔或配置缺失宝塔,在一键登录前会触发 session cleanup TTL 兜底检测/修复。
6. 多台宝塔同时打开,2 小时后刷新不应因为 `task.py hardcoded_3600` 陆续掉线。
7. 线上页面不显示全局 API Key 明文。
## 回滚点
发布前建议备份:
```text
server/application/services/btpanel_service.py
server/infrastructure/btpanel/bootstrap_lock.py
server/infrastructure/btpanel/login_url_lock.py
server/infrastructure/btpanel/ssh_bootstrap.py
server/main.py
web/app-v2/
Dockerfile
Dockerfile.prod
```
如果发布后宝塔一键登录异常:
1. 先回滚上述后端文件。
2. 重启后端服务。
3. 保留 `/app-v2/` 不影响旧 `/app/`,必要时可以只移除 `web/app-v2/` 静态目录或取消 `server/main.py` 挂载。
## 2026-07-08 validate-only verification
The server-side release applier now supports validate-only mode.
Command verified locally with Git Bash:
```bash
bash deploy/apply-release-bundle.sh --validate-only --tarball /c/Users/uzuma/Documents/Codex/2026-07-06/yuu/outputs/nexus-release-20260708.tar.gz --sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
`
Result:
` ext
SHA256 OK: 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
Extracted release tree validation OK
Validate-only mode: tarball checksum and content validation passed
`
Network/SSH production execution is still not performed because Codex approval auto-review returned HTTP 404 for read-only checks.
## 2026-07-08 one-command local artifact preparation
A single local command now prepares and verifies the release artifact end-to-end:
`cmd
.venv-py312-codex\Scripts\python.exe scripts\prepare_release_artifact.py --old-snapshot C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\work\nexus_code_context\snapshots\nexus-source-20260707-070236 --output-dir C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs --name nexus-release-20260708
`
Verified result:
` ext
[prepare-release-artifact] OK
added=381 modified=26 deleted=347
files=2409
entries=2409
size_mb=27.02
sha256=46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b
60 passed in 1.70s
`
## 2026-07-08 release index and checksum file
Release index:
` ext
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\Nexus-release-index-20260708.md
`
Standard SHA256 checksum file:
` ext
C:\Users\uzuma\Documents\Codex\2026-07-06\yuu\outputs\nexus-release-20260708.tar.gz.sha256
`
The checksum file contains:
` ext
46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b nexus-release-20260708.tar.gz
`
## Server preflight command
Before applying the release on the production host, run:
```bash
bash /tmp/nexus-release/deploy/preflight-release-host.sh \
--tarball /tmp/nexus-release-20260708.tar.gz \
--sha256 46299832fb78e8cc196f85cf1928f34eea4deee82dbc83b67883394f0359036b \
--deploy-path /opt/nexus
```
This checks required commands, checksum, release tar paths, deploy path, runtime hints, disk space, and current local endpoints before applying changes.

Some files were not shown because too many files have changed in this diff Show More