5 Commits

Author SHA1 Message Date
Codex Release Bot 8363746cf6 docs: clarify release bundle hash verification 2026-07-09 05:14:29 +08:00
Codex Release Bot b49dad8bda chore: ignore local codex virtualenvs 2026-07-09 05:14:04 +08:00
Codex Release Bot 279259092d release: publish gitea allowlist and harden bundle deploy 2026-07-09 05:12:54 +08:00
Codex Release Bot 74149e0d90 docs: add nexus release merge guide 2026-07-09 00:14:19 +08:00
Codex Release Bot 1933f0af6e release: nexus btpanel session fix and app-v2 2026-07-08 22:31:31 +08:00
6005 changed files with 41007 additions and 96764 deletions
+44 -44
View File
@@ -1,44 +1,44 @@
---
description: 减少盲目搜文件、控制 Read 次数与 token 消耗
alwaysApply: true
---
# Agent 探索约束(省 token
## 开始前
1. 读根目录 `AGENTS.md` 的「代码地图」与「问题 → 文件」表
2. 架构/API 全览只读 `docs/project/nexus-functional-development-guide.md`(或用户指定的 §),禁止为找结构全库 Grep
3. 用户已给出路径/文件名时,只读该路径,禁止扩大范围
## 搜索顺序
```
AGENTS.md / 功能指南 § → Grep(带 path)→ Read(≤3 个文件)→ 仍不够再扩一圈
```
- **禁止**无 `path` 的全仓库 `Grep` / `Glob`(除非用户明确要求全库)
- **Grep 优先于 Read**Glob 只用于定位文件名,不逐个 Read 全部匹配
- 同一任务 **Read 不超过 5 个文件**;找到答案后立即停止
- 不要重复 Read 已在本对话出现过的文件
## 分层定位(禁止跨层乱搜)
| 层 | 路径 |
|----|------|
| HTTP 路由 | `server/api/<module>.py` |
| 业务 | `server/application/services/` |
| 数据访问 | `server/infrastructure/` |
| 模型 | `server/domain/models/` |
| 前端页 | `frontend/src/pages/` |
| 前端路由 | `frontend/src/router/index.ts` |
| 前端 API 客户端 | `frontend/src/api/` |
真实前端源码在 `frontend/src/`,不是 `web/app/*.html`(那是构建产物)。
## 文档索引
- 功能 SSOT`docs/project/nexus-functional-development-guide.md`
- 文档导航:`docs/README.md`
- 本地路径/MCP`docs/project/linux-dev-paths.md`
- 运维(连机/exec):用户 Skill `Nexus平台` 或 `docs/project/nexus-1panel-operations-knowledge.md`
---
description: 减少盲目搜文件、控制 Read 次数与 token 消耗
alwaysApply: true
---
# Agent 探索约束(省 token
## 开始前
1. 读根目录 `AGENTS.md` 的「代码地图」与「问题 → 文件」表
2. 架构/API 全览只读 `docs/project/nexus-functional-development-guide.md`(或用户指定的 §),禁止为找结构全库 Grep
3. 用户已给出路径/文件名时,只读该路径,禁止扩大范围
## 搜索顺序
```
AGENTS.md / 功能指南 § → Grep(带 path)→ Read(≤3 个文件)→ 仍不够再扩一圈
```
- **禁止**无 `path` 的全仓库 `Grep` / `Glob`(除非用户明确要求全库)
- **Grep 优先于 Read**Glob 只用于定位文件名,不逐个 Read 全部匹配
- 同一任务 **Read 不超过 5 个文件**;找到答案后立即停止
- 不要重复 Read 已在本对话出现过的文件
## 分层定位(禁止跨层乱搜)
| 层 | 路径 |
|----|------|
| HTTP 路由 | `server/api/<module>.py` |
| 业务 | `server/application/services/` |
| 数据访问 | `server/infrastructure/` |
| 模型 | `server/domain/models/` |
| 前端页 | `frontend/src/pages/` |
| 前端路由 | `frontend/src/router/index.ts` |
| 前端 API 客户端 | `frontend/src/api/` |
真实前端源码在 `frontend/src/`,不是 `web/app/*.html`(那是构建产物)。
## 文档索引
- 功能 SSOT`docs/project/nexus-functional-development-guide.md`
- 文档导航:`docs/README.md`
- 本地路径/MCP`docs/project/linux-dev-paths.md`
- 运维(连机/exec):用户 Skill `Nexus平台` 或 `docs/project/nexus-1panel-operations-knowledge.md`
+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/`
## 进度条(改代码时输出)
+40 -29
View File
@@ -1,29 +1,40 @@
.git
.gitignore
.claude
.cursor
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
.coverage
htmlcov/
frontend/node_modules
frontend/dist
node_modules
.env
docker/.env
docker/runtime
tests/
docs/
standards/
*.md
!docker/README.md
web/uploads
backups/
deploy/gate_log.jsonl
.git
.gitignore
.claude
.cursor
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.mypy_cache
**/.ruff_cache
.coverage
htmlcov/
frontend/node_modules
frontend-v2/node_modules
frontend/dist
node_modules
.venv
.venv-*
venv
.pytest_cache
.ruff_cache
.playwright-mcp
2025.2
=2025.2
*.bak-
.env
docker/.env
docker/runtime
tmp/
tests/
docs/
standards/
*.md
!docker/README.md
web/uploads
backups/
deploy/gate_log.jsonl
-63
View File
@@ -1,63 +0,0 @@
# Nexus Environment Configuration
# Copy this file to .env and modify values for your deployment.
# Or run the web installer at /app/install.html to auto-generate this file.
#
# Generate secret keys: openssl rand -hex 32
# ── Brand ──
NEXUS_SYSTEM_NAME=Nexus
NEXUS_SYSTEM_TITLE=Nexus — 服务器运维管理平台
# ── Server ──
NEXUS_HOST=0.0.0.0
NEXUS_PORT=8600
# ── Deployment (auto-set by installer) ──
NEXUS_DEPLOY_PATH=/opt/nexus
# Comma-separated frontend origins for CORS
NEXUS_CORS_ORIGINS=http://localhost:8600
# Base URL for Agent heartbeat reporting
NEXUS_API_BASE_URL=http://localhost:8600
# ── Database (MySQL, REQUIRED) ──
NEXUS_DATABASE_URL=mysql+aiomysql://root:password@127.0.0.1:3306/nexus
# Pool params: auto-calculated by installer from MySQL max_connections
# Formula: pool_size = max(20, maxConn*0.4), overflow = max(20, maxConn*0.3)
# Default below matches MySQL max_connections=400 (160+120=280 max)
# install wizard will overwrite these with values tuned to your MySQL instance
NEXUS_DB_POOL_SIZE=160
NEXUS_DB_MAX_OVERFLOW=120
# ── Security (REQUIRED — generate with: openssl rand -hex 32) ──
NEXUS_SECRET_KEY=change-me-use-openssl-rand-hex-32
NEXUS_API_KEY=change-me-use-openssl-rand-hex-32
NEXUS_ENCRYPTION_KEY=
# ── Redis (REQUIRED) ──
NEXUS_REDIS_URL=redis://127.0.0.1:6379/0
# ── SSH ──
NEXUS_SSH_STRICT_HOST_CHECKING=false
# ── Alert Thresholds (%) ──
NEXUS_CPU_ALERT_THRESHOLD=80
NEXUS_MEM_ALERT_THRESHOLD=80
NEXUS_DISK_ALERT_THRESHOLD=80
# ── Health Check ──
NEXUS_HEALTH_CHECK_INTERVAL=60
# ── Telegram Alerts (optional — configure in Settings UI) ──
NEXUS_TELEGRAM_BOT_TOKEN=
NEXUS_TELEGRAM_CHAT_ID=
# ── MySQL MCP (@yclenove/mysql-mcp-server — run: python scripts/sync_mysql_mcp_env.py) ──
# Cursor 从项目根 .env 读取;与 NEXUS_DATABASE_URL 保持一致;默认只读
# MYSQL_HOST=127.0.0.1
# MYSQL_PORT=3306
# MYSQL_USER=root
# MYSQL_PASSWORD=password
# MYSQL_DATABASE=nexus
# MYSQL_READONLY=true # production / MCP read-only
# MYSQL_READONLY=false # local dev: full DML/DDL via MCP
# MYSQL_DATABASE_ALLOWLIST=nexus
+90 -89
View File
@@ -1,89 +1,90 @@
# Nexus — Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
build/
.eggs/
# Environment (NEVER commit — contains production credentials)
.env
.install_locked
.install_state.json
SECRETS.md
.git-askpass-nexus.sh
deploy/nexus-1panel.secrets.sh
*.pem
.env.*
.venv/
venv/
ENV/
# Agent memory (local MCP persistence, do not commit)
.claude/mcp-memory.jsonl
# MCP tool data (local development artifacts)
.megamemory/
.playwright-mcp/
frontend/test-results/
frontend/e2e/.auth/
tmp/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
nexus.log
# Node offline cache for WSL install (large tarball)
.cache/
# Test
.pytest_cache/
htmlcov/
.coverage
coverage.xml
# Node (for Tailwind/Alpine.js build)
node_modules/
web/css/tailwind-output.css
# Build output (Vite/Vuetify frontend)
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh
# 仍忽略本地临时文件
web/app/assets/.tmp/
# Legacy dist dir (no longer used)
**/dist/
# Uploads
web/uploads/
# Data (sensitive)
web/data/config.php
web/data/*.db
web/data/1panel-hosts.json
# Backups
backups/
# Docker local secrets
docker/.env
docker/.env.prod
!docker/.env.example
!docker/.env.prod.example
scripts/npm-proxy.env
# Deploy
deploy/*.key
# Backups
backups/
# Nexus — Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
build/
.eggs/
# Environment (NEVER commit — contains production credentials)
.env
.install_locked
.install_state.json
SECRETS.md
.git-askpass-nexus.sh
deploy/nexus-1panel.secrets.sh
*.pem
.env.*
.venv/
.venv-*/
venv/
ENV/
# Agent memory (local MCP persistence, do not commit)
.claude/mcp-memory.jsonl
# MCP tool data (local development artifacts)
.megamemory/
.playwright-mcp/
frontend/test-results/
frontend/e2e/.auth/
tmp/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
nexus.log
# Node offline cache for WSL install (large tarball)
.cache/
# Test
.pytest_cache/
htmlcov/
.coverage
coverage.xml
# Node (for Tailwind/Alpine.js build)
node_modules/
web/css/tailwind-output.css
# Build output (Vite/Vuetify frontend)
# SPA 构建产物纳入 Git,便于服务器 git pull 部署(见 deploy/deploy-frontend.sh
# 仍忽略本地临时文件
web/app/assets/.tmp/
# Legacy dist dir (no longer used)
**/dist/
# Uploads
web/uploads/
# Data (sensitive)
web/data/config.php
web/data/*.db
web/data/1panel-hosts.json
# Backups
backups/
# Docker local secrets
docker/.env
docker/.env.prod
!docker/.env.example
!docker/.env.prod.example
scripts/npm-proxy.env
# Deploy
deploy/*.key
# Backups
backups/
+31
View File
@@ -0,0 +1,31 @@
---
name: nexus-btpanel-review
description: Use when reviewing or changing Nexus BT/宝塔 panel integration, one-click login, API bootstrap, session keepalive fixes, or BT credential handling.
---
# Nexus BT Panel Review
Use for 宝塔/BT panel integration changes.
## Call chain to map
API one-click login -> btpanel_service -> BT API credential lookup/bootstrap -> SSH bootstrap if needed -> login URL/tmp_token -> panel session behavior.
## Required checks
- Do not log or return BT API keys, session cookies, temporary login tokens, SSH private keys, or passwords.
- One-click login should fail closed for real authentication errors, but best-effort environment repair must not block login unless required for correctness.
- Session keepalive/cleanup repair must be idempotent and throttled; do not restart or rewrite BT panel aggressively without explicit approval.
- Respect existing BT panel services and ports. Do not occupy panel, nginx, redis, or mysql ports.
- Old servers with existing BT API credentials must still receive the session-cleanup TTL check before login.
- Record bootstrap state without storing secrets.
## Regression tests
Cover:
- Existing credentials still trigger throttled TTL check.
- Missing SSH auth does not block login URL generation.
- Failed repair is throttled and retried later.
- Temporary login token TTL remains bounded.
- No secret values appear in returned bootstrap/status payloads.
+40
View File
@@ -0,0 +1,40 @@
---
name: nexus-security-review
description: Use when reviewing or changing Nexus backend/API/security-sensitive code. Focuses on FastAPI admin APIs, SQLAlchemy data access, SSH operations, file manager boundaries, logging redaction, and regression tests.
---
# Nexus Security Review
Use this skill before changing Nexus backend/API code or doing a security pass.
## Required workflow
1. Identify entrypoints: FastAPI router, service method, DB model/session, Redis/lock usage, SSH/BT panel call.
2. Draw the request path in notes: API -> schema -> service -> DB/Redis/SSH/external.
3. Check auth first: admin-only endpoints must depend on the current admin dependency; background/admin tools must still preserve audit logs.
4. Validate every user-controlled value at the schema/API boundary and again before shell/path-sensitive sinks.
5. For every fix, add pytest coverage for the dangerous case and the allowed case.
6. Run focused tests, then full pytest when practical, then git diff --check.
7. Update the Nexus Markdown report with changed files, tests, and residual risks.
## Nexus-specific rules
- Do not print secrets: passwords, SSH keys, tokens, cookies, API keys, .env values, BT panel credentials.
- script_service is an intentional administrator remote shell. Do not report arbitrary shell execution there as a vulnerability. Review only auth, audit trail, timeout, output truncation, and redaction.
- File manager APIs must not unexpectedly operate outside the target path/server chosen by the admin UI.
- Any remote shell command must use shlex.quote; add -- where command options can be confused with filenames.
- Archive extraction must list and validate members before extraction; reject absolute paths, .., backslashes, and special file types.
- Error responses must not expose stack traces or secrets.
- DB access should use SQLAlchemy parameters/query builder, not raw string interpolation.
## Review output
For each finding record:
- Severity: Critical/High/Medium/Low/Hardening
- Call chain
- Evidence file/function
- Exploit or failure boundary
- Fix summary
- Test command/result
- Commit hash if committed
+29
View File
@@ -0,0 +1,29 @@
---
name: nexus-ssh-safety
description: Use when reviewing or editing Nexus code that executes commands on managed servers over SSH, including file manager, transfer, archive, agent install, and BT panel bootstrap flows.
---
# Nexus SSH Safety
Use for any Nexus feature that builds shell commands or transfers files over SSH.
## Command construction rules
- Prefer argument arrays/local library APIs when available; otherwise quote every dynamic shell token with shlex.quote.
- Insert -- before filename/path operands for commands that parse options:
m, mv, cp, ar, zip, chmod, chown, etc.
- Never concatenate unquoted user input into shell command strings.
- Keep timeouts explicit for network/SSH work; avoid unbounded commands.
- Truncate or sanitize command output before returning it to API clients.
- Do not log secrets or full credential-bearing URLs.
## Path and archive rules
- Normalize paths with POSIX semantics for remote Linux paths.
- Reject empty paths where dangerous, root/system paths for recursive destructive operations, and traversal where the feature expects a bounded directory.
- Archive creation must protect member names beginning with -.
- Archive extraction must validate member names and types before extraction.
- Temporary files/directories must be unique and cleaned best-effort.
## script_service exception
+5
View File
@@ -0,0 +1,5 @@
Collecting tzdata
Downloading tzdata-2026.2-py2.py3-none-any.whl.metadata (1.4 kB)
Downloading tzdata-2026.2-py2.py3-none-any.whl (349 kB)
Installing collected packages: tzdata
Successfully installed tzdata-2026.2
+5 -1
View File
@@ -40,11 +40,15 @@ cd frontend && npm run dev # :3000/app/
## 部署
默认 **本机 rsync → SSH 生产机**(不 push Gitea):
```bash
bash deploy/pre_deploy_check.sh
bash deploy/deploy-production.sh # 需 SSH nexus + 用户批准
bash deploy/deploy-production.sh # rsync + Docker upgrade --skip-git + 前端
```
可选:`NEXUS_DEPLOY_VIA_GIT=1` 恢复远程 `git pull`。仅 `git push` 当你明确要求时。
详情见功能指南 §1718。
## 归档文档
+46 -33
View File
@@ -1,33 +1,46 @@
# Nexus 6.0 API + 静态 SPA (web/app)
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/app/ ./web/app/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
# Nexus 6.0 - API + static SPA (web/app)
FROM python:3.12-slim-bookworm
ARG DEBIAN_MIRROR=https://mirrors.aliyun.com/debian
ARG DEBIAN_SECURITY_MIRROR=https://mirrors.aliyun.com/debian-security
ARG PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
ARG PIP_TRUSTED_HOST=mirrors.aliyun.com
RUN set -eux; \
sed -i \
-e "s|http://deb.debian.org/debian-security|${DEBIAN_SECURITY_MIRROR}|g" \
-e "s|http://deb.debian.org/debian|${DEBIAN_MIRROR}|g" \
/etc/apt/sources.list.d/debian.sources; \
apt-get update; \
apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass; \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --index-url "${PIP_INDEX_URL}" --trusted-host "${PIP_TRUSTED_HOST}" -r requirements.txt
COPY server/ ./server/
COPY web/app/ ./web/app/
COPY web/app-v2/ ./web/app-v2/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
+77 -64
View File
@@ -1,64 +1,77 @@
# Nexus 6.0 production image (frontend build + API)
# Build: docker compose -f docker/docker-compose.prod.yml build
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
# ── Stage 1: Vue/Vite SPA ──
FROM node:20-bookworm-slim AS frontend
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --ignore-scripts
COPY frontend/ ./
RUN npx vite build
# ── Stage 2: Python API + built static assets ──
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/agent/ ./web/agent/
COPY --from=frontend /build/web/app ./web/app/
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
COPY web/app/wallpapers/ ./web/app/wallpapers/
# Install wizard + static SPA assets not emitted by Vite
COPY web/app/install.html ./web/app/install.html
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
RUN mkdir -p ./web/app/vendor
COPY web/app/vendor/tailwindcss-browser.js \
web/app/vendor/theme-init.js \
web/app/vendor/theme.css \
web/app/vendor/alpinejs.min.js \
./web/app/vendor/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh \
&& test -f /app/web/app/install.html \
&& test -f /app/web/app/vendor/alpinejs.min.js \
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
&& test -f /app/web/app/vendor/theme.css \
&& test -f /app/web/app/vendor/theme-init.js
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
# Nexus 6.0 鈥?production image (frontend build + API)
# Build: docker compose -f docker/docker-compose.prod.yml build
# See: docs/design/plans/2026-06-04-1panel-docker-production.md
# 鈹€鈹€ Stage 1: Vue/Vite SPA 鈹€鈹€
FROM node:20-bookworm-slim AS frontend
WORKDIR /build/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --ignore-scripts
COPY frontend/ ./
RUN npx vite build
# Stage 1b: React/Vite App V2
FROM node:20-bookworm-slim AS frontend_v2
WORKDIR /build/frontend-v2
COPY frontend-v2/package.json frontend-v2/package-lock.json* ./
RUN if [ -f package-lock.json ]; then npm ci --ignore-scripts; else npm install --ignore-scripts; fi
COPY frontend-v2/ ./
RUN npx vite build
# 鈹€鈹€ Stage 2: Python API + built static assets 鈹€鈹€
FROM python:3.12-slim-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
openssh-client \
rsync \
sshpass \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server/ ./server/
COPY web/agent/ ./web/agent/
COPY --from=frontend /build/web/app ./web/app/
COPY --from=frontend_v2 /build/web/app-v2 ./web/app-v2/
# Login wallpaper cache (Vite does not emit this dir; sync also writes here at runtime)
COPY web/app/wallpapers/ ./web/app/wallpapers/
# Install wizard + static SPA assets not emitted by Vite
COPY web/app/install.html ./web/app/install.html
COPY web/app/servers_import_template.csv ./web/app/servers_import_template.csv
RUN mkdir -p ./web/app/vendor
COPY web/app/vendor/tailwindcss-browser.js \
web/app/vendor/theme-init.js \
web/app/vendor/theme.css \
web/app/vendor/alpinejs.min.js \
./web/app/vendor/
COPY docker/entrypoint.sh ./docker/entrypoint.sh
RUN chmod +x ./docker/entrypoint.sh \
&& test -f /app/web/app/install.html \
&& test -f /app/web/app/vendor/alpinejs.min.js \
&& test -f /app/web/app/vendor/tailwindcss-browser.js \
&& test -f /app/web/app/vendor/theme.css \
&& test -f /app/web/app/vendor/theme-init.js
ENV PYTHONPATH=/app \
PYTHONUNBUFFERED=1 \
NEXUS_HOST=0.0.0.0 \
NEXUS_PORT=8600
EXPOSE 8600
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
CMD curl -sf http://127.0.0.1:8600/health | grep -q '^ok' || exit 1
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["uvicorn", "server.main:app", "--host", "0.0.0.0", "--port", "8600"]
+71 -71
View File
@@ -1,71 +1,71 @@
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
# Domain: api.synaglobal.vip
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
#
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
upstream nexus_api {
server 127.0.0.1:8600;
keepalive 32;
}
# WebSocket log without query string (JWT must not hit access_log)
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
client_max_body_size 500m;
client_body_timeout 600s;
location /api/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 10s;
}
location /ws/ {
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /health {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
location /app/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
}
location /agent/ {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
# Fallback: install mode or misc routes served by FastAPI
location / {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
}
# 1Panel OpenResty — Nexus reverse proxy (paste into site custom config)
# Domain: api.synaglobal.vip
# Upstream: Nexus Docker on 127.0.0.1:8600 (docker-compose.prod.yml)
#
# SSL: configure in 1Panel UI (Let's Encrypt). This snippet assumes HTTPS server block.
upstream nexus_api {
server 127.0.0.1:8600;
keepalive 32;
}
# WebSocket log without query string (JWT must not hit access_log)
log_format nexus_ws '$remote_addr - [$time_local] "$request_method $uri" $status $body_bytes_sent';
client_max_body_size 500m;
client_body_timeout 600s;
location /api/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 10s;
}
location /ws/ {
access_log /www/sites/api.synaglobal.vip/log/ws.log nexus_ws;
proxy_pass http://nexus_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /health {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
location /app/ {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
}
location /agent/ {
proxy_pass http://nexus_api;
proxy_set_header Host $host;
}
# Fallback: install mode or misc routes served by FastAPI
location / {
proxy_pass http://nexus_api;
proxy_http_version 1.1;
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;
}
+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 "$@"
+69 -26
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,38 +104,69 @@ 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"
echo "▶ Sync host web/app into container..."
echo "▶ Frontend V2: local vite build:safe + upload to host web/app-v2..."
cd frontend-v2 && npm run build:safe && cd ..
tar czf /tmp/nexus-frontend-v2.tar.gz -C web/app-v2 index.html assets/
scp_cmd /tmp/nexus-frontend-v2.tar.gz "${NEXUS_SSH_HOST}:/tmp/nexus-frontend-v2.tar.gz"
ssh_cmd "sudo mkdir -p ${DEPLOY_PATH}/web/app-v2 && cd ${DEPLOY_PATH}/web/app-v2 && sudo tar xzf /tmp/nexus-frontend-v2.tar.gz && rm /tmp/nexus-frontend-v2.tar.gz && sudo chown -R ${DEPLOY_CHOWN} ${DEPLOY_PATH}/web/app-v2"
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 and web/app-v2 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
}
@@ -136,9 +177,11 @@ PORT="$(ssh_cmd "grep -E '^NEXUS_PUBLISH_PORT=' ${DEPLOY_PATH}/docker/.env.prod
PORT="${PORT:-8600}"
HEALTH=$(ssh_cmd "curl -s http://127.0.0.1:${PORT}/health" 2>/dev/null || true)
SPA=$(ssh_cmd "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:${PORT}/app/" 2>/dev/null || echo "000")
SPA_V2=$(ssh_cmd "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/ → ${SPA}"
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" ]]; then
echo " /app-v2/ → ${SPA_V2}"
if [[ "${HEALTH}" == "ok" && "${SPA}" == "200" && "${SPA_V2}" == "200" ]]; then
echo "✓ Deploy successful"
if ssh_cmd "command -v crontab >/dev/null && crontab -l 2>/dev/null | grep -q nexus-health-monitor" 2>/dev/null; then
echo "✓ Ops cron (health_monitor) already installed"
+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"}
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
-21
View File
@@ -1,21 +0,0 @@
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
# 用于 git pushscripts/git-push.sh);clone/pull 仍可匿名
# NEXUS_GITEA_HOST='66.154.115.8:3000'
# NEXUS_GITEA_REPO='admin/Nexus.git'
# admin 账号密码或 Access Token
# NEXUS_GITEA_TOKEN='你的密码或令牌'
# 新机安装勿填下面三项(install-nexus-fresh / --fresh 会自动按机生成)
# NEXUS_SECRET_KEY=''
# NEXUS_API_KEY=''
# NEXUS_ENCRYPTION_KEY=''
NEXUS_DOMAIN='api.synaglobal.vip'
NEXUS_ROOT='/opt/nexus'
NEXUS_PROFILE='2c8g'
NEXUS_FROM_ENV='/www/wwwroot/api.synaglobal.vip/.env'
# 本机生产 SSHdeploy-production.sh 读取;私钥路径勿提交真实 .pem)
# NEXUS_SSH='azureuser@20.24.218.235'
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
Executable → Regular
+26 -5
View File
@@ -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"
}
@@ -1018,10 +1023,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 +1147,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 +1162,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 +1179,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 +1292,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 ;;
Executable → Regular
View File
+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 "$@"
Executable → Regular
View File
+112
View File
@@ -0,0 +1,112 @@
#!/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='frontend-v2/node_modules'
--exclude='node_modules'
--exclude='.venv'
--exclude='.venv-*'
--exclude='venv'
--exclude='__pycache__'
--exclude='.pytest_cache'
--exclude='.ruff_cache'
--exclude='.cursor'
--exclude='.playwright-mcp'
--exclude='.install_locked'
--exclude='.install_state.json'
--exclude='web/app/assets'
--exclude='tmp'
--exclude='2025.2'
--exclude='=2025.2'
--exclude='*.bak-'
--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})"
View File
+34 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Sync Git-tracked web/app into the running Nexus Docker container.
# Sync Git-tracked web/app and web/app-v2 into the running Nexus Docker container.
#
# Root cause: Dockerfile.prod runs vite build inside the image; Docker layer cache
# can produce asset hashes that differ from committed web/app on the host.
@@ -51,8 +51,25 @@ verify_entry_bundle() {
info "入口校验 OK: ${bundle_path} → HTTP 200"
}
verify_app_v2_entry_bundle() {
local html bundle_path code
html="$(curl -sf "http://127.0.0.1:${NEXUS_PUBLISH_PORT}/app-v2/" 2>/dev/null || true)"
bundle_path="$(printf '%s' "$html" | sed -n 's/.*src="\(\/app-v2\/assets\/[^"]*\.js\)".*/\1/p' | head -1)"
if [[ -z "$bundle_path" ]]; then
error "/app-v2/ index.html 未解析到主 bundle"
exit 1
fi
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${NEXUS_PUBLISH_PORT}${bundle_path}" 2>/dev/null || echo 000)"
if [[ "$code" != "200" ]]; then
error "V2 主 bundle ${bundle_path} → HTTP ${code}"
exit 1
fi
info "V2 入口校验 OK: ${bundle_path} → HTTP 200"
}
main() {
local app="${NEXUS_ROOT}/web/app"
local app_v2="${NEXUS_ROOT}/web/app-v2"
if [[ ! -f "${app}/index.html" ]]; then
error "缺少 ${app}/index.html,请先 git pull"
exit 1
@@ -61,6 +78,14 @@ main() {
error "缺少 ${app}/assets"
exit 1
fi
if [[ ! -f "${app_v2}/index.html" ]]; then
error "缺少 ${app_v2}/index.html,请先构建 frontend-v2"
exit 1
fi
if [[ ! -d "${app_v2}/assets" ]]; then
error "缺少 ${app_v2}/assets"
exit 1
fi
resolve_container
@@ -74,6 +99,12 @@ main() {
docker_cmd cp "${app}/favicon.ico" "${CONTAINER}:/app/web/app/favicon.ico" 2>/dev/null || true
fi
info "同步 web/app-v2 → 容器 /app/web/app-v2 ..."
docker_cmd exec "$CONTAINER" rm -rf /app/web/app-v2/assets
docker_cmd exec "$CONTAINER" mkdir -p /app/web/app-v2
docker_cmd cp "${app_v2}/index.html" "${CONTAINER}:/app/web/app-v2/index.html"
docker_cmd cp "${app_v2}/assets" "${CONTAINER}:/app/web/app-v2/assets"
if [[ -x "${NEXUS_ROOT}/deploy/prune_frontend_assets.py" ]]; then
info "容器内 prune 未引用 assets(可选)..."
docker_cmd exec "$CONTAINER" python3 /app/deploy/prune_frontend_assets.py --dry-run 2>/dev/null || \
@@ -81,7 +112,8 @@ main() {
fi
verify_entry_bundle
info "web/app 同步完成"
verify_app_v2_entry_bundle
info "web/app 与 web/app-v2 同步完成"
}
main "$@"
View File
View File
Executable → Regular
View File
Executable → Regular
View File
+15 -8
View File
@@ -1,8 +1,8 @@
# Nexus 6.0 Docker Compose (dev / staging)
# Nexus 6.0 鈥?Docker Compose (dev / staging)
# Usage:
# cp docker/.env.example docker/.env # or: python docker/generate_env.py
# docker compose up -d --build
# open http://localhost:8600/app/
# docker compose --env-file docker/.env up -d --build
# open http://localhost:18600/app/
name: nexus
@@ -24,7 +24,7 @@ services:
retries: 8
start_period: 40s
ports:
- "${MYSQL_PUBLISH_PORT:-3306}:3306"
- "${MYSQL_PUBLISH_PORT:-13306}:3306"
redis:
image: redis:7-alpine
@@ -38,12 +38,18 @@ services:
timeout: 3s
retries: 8
ports:
- "${REDIS_PUBLISH_PORT:-6379}:6379"
- "${REDIS_PUBLISH_PORT:-16379}:6379"
nexus:
build:
context: .
dockerfile: Dockerfile
network: host
args:
DEBIAN_MIRROR: https://mirrors.aliyun.com/debian
DEBIAN_SECURITY_MIRROR: https://mirrors.aliyun.com/debian-security
PIP_INDEX_URL: https://mirrors.aliyun.com/pypi/simple/
PIP_TRUSTED_HOST: mirrors.aliyun.com
restart: unless-stopped
depends_on:
mysql:
@@ -51,7 +57,7 @@ services:
redis:
condition: service_healthy
ports:
- "${NEXUS_PUBLISH_PORT:-8600}:8600"
- "${NEXUS_PUBLISH_PORT:-18600}:8600"
environment:
MYSQL_HOST: mysql
MYSQL_PORT: "3306"
@@ -64,11 +70,11 @@ services:
NEXUS_DEPLOY_PATH: /app
NEXUS_DATABASE_URL: mysql+aiomysql://nexus:${MYSQL_PASSWORD:-nexus_dev_pass}@mysql:3306/nexus
NEXUS_REDIS_URL: redis://redis:6379/0
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:8600,http://127.0.0.1:8600}
NEXUS_CORS_ORIGINS: ${NEXUS_CORS_ORIGINS:-http://localhost:18600,http://127.0.0.1:18600,http://localhost:8600,http://127.0.0.1:8600}
NEXUS_SECRET_KEY: ${NEXUS_SECRET_KEY:-}
NEXUS_API_KEY: ${NEXUS_API_KEY:-}
NEXUS_ENCRYPTION_KEY: ${NEXUS_ENCRYPTION_KEY:-}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:8600}
NEXUS_API_BASE_URL: ${NEXUS_API_BASE_URL:-http://localhost:18600}
env_file:
- path: docker/.env
required: false
@@ -87,3 +93,4 @@ volumes:
redis-data:
nexus-state:
nexus-web-data:
-18
View File
@@ -1,18 +0,0 @@
# Copy to docker/.env (or run: python docker/generate_env.py)
# Used by docker compose for nexus service — never commit docker/.env
MYSQL_ROOT_PASSWORD=change_me_root
MYSQL_PASSWORD=change_me_nexus
# Leave empty to use install wizard at http://localhost:8600/app/install.html
# Or run generate_env.py to fill these three:
NEXUS_SECRET_KEY=
NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY=
NEXUS_CORS_ORIGINS=http://localhost:8600,http://127.0.0.1:8600
NEXUS_API_BASE_URL=http://localhost:8600
# Optional host port overrides
# NEXUS_PUBLISH_PORT=8600
# MYSQL_PUBLISH_PORT=3306
-39
View File
@@ -1,39 +0,0 @@
# Production Compose env — copy to docker/.env.prod (never commit)
# Fresh install: bash deploy/install-nexus-fresh.sh (auto-generates NEXUS keys per host)
# MySQL / Redis 不在 Docker 栈 — 请在宿主机或 1Panel 自建,安装向导步骤 3 填写
# Auto-generated on fresh install (unique per server). Do NOT copy between hosts.
NEXUS_SECRET_KEY=
NEXUS_API_KEY=
NEXUS_ENCRYPTION_KEY=
# 1 = entrypoint must not write /app/.env until install wizard step 3
NEXUS_INSTALL_WIZARD_PENDING=0
# Public URL
NEXUS_CORS_ORIGINS=https://api.synaglobal.vip
NEXUS_API_BASE_URL=https://api.synaglobal.vip
# Nexus container resource profile (docker/profiles/*.env)
NEXUS_DB_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20
# 1Panelnx update 自动探测并写入(安装向导步骤 3 预填)
# NEXUS_1PANEL_DB_HOST=1Panel-mysql-xxxx
# NEXUS_1PANEL_REDIS_HOST=1Panel-redis-xxxx
# 宿主机 Compose 插值/ nx update 同步用;安装向导完成后以卷内 /app/.env 为准(不经 Compose 注入)
# 1Panel 有密码时: redis://:你的密码@1Panel-redis-xxxx:6379/0
NEXUS_REDIS_URL=redis://host.docker.internal:6379/0
# 宿主机 git 仓库路径(容器内安装向导用于提示 nx cron / nx update
NEXUS_HOST_ROOT=/opt/nexus
# Optional — production probe / E2E only (host .env.prod; NOT for API runtime; never commit values)
# NEXUS_TEST_ADMIN_PASSWORD — must match admin login password
# NEXUS_PROBE_CLIENT_IP — optional; IP in login allowlist for origin probe (auto-read from DB if unset)
# NEXUS_TEST_ADMIN_PASSWORD=
# NEXUS_PROBE_CLIENT_IP=
# Optional
# NEXUS_PUBLISH_PORT=8600
+20 -20
View File
@@ -1,20 +1,20 @@
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
max_connections = 80
innodb_read_io_threads = 2
innodb_write_io_threads = 2
table_open_cache = 1000
tmp_table_size = 32M
max_heap_table_size = 32M
[client]
default-character-set = utf8mb4
# MySQL 8.4 — Nexus production tuning for 1 vCPU / 4 GiB host (minimal)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 512M
innodb_log_file_size = 128M
max_connections = 80
innodb_read_io_threads = 2
innodb_write_io_threads = 2
table_open_cache = 1000
tmp_table_size = 32M
max_heap_table_size = 32M
[client]
default-character-set = utf8mb4
+25 -25
View File
@@ -1,25 +1,25 @@
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
innodb_buffer_pool_size = 1024M
innodb_log_file_size = 256M
# Nexus async pool default 30+20; leave headroom for admin/migrations
max_connections = 120
# 2 vCPU: avoid aggressive IO threads
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# Sensible defaults on small VMs
table_open_cache = 2000
tmp_table_size = 64M
max_heap_table_size = 64M
[client]
default-character-set = utf8mb4
# MySQL 8.4 — Nexus production tuning for 2 vCPU / 8 GiB host
# Mounted by docker/docker-compose.prod.yml (see docs/design/plans/2026-06-04-1panel-docker-production.md)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# ~1 GiB for InnoDB (leave RAM for Nexus, Redis, 1Panel, OS)
innodb_buffer_pool_size = 1024M
innodb_log_file_size = 256M
# Nexus async pool default 30+20; leave headroom for admin/migrations
max_connections = 120
# 2 vCPU: avoid aggressive IO threads
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# Sensible defaults on small VMs
table_open_cache = 2000
tmp_table_size = 64M
max_heap_table_size = 64M
[client]
default-character-set = utf8mb4
+20 -20
View File
@@ -1,20 +1,20 @@
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 4096M
innodb_log_file_size = 512M
max_connections = 200
innodb_read_io_threads = 8
innodb_write_io_threads = 8
table_open_cache = 4000
tmp_table_size = 128M
max_heap_table_size = 128M
[client]
default-character-set = utf8mb4
# MySQL 8.4 — Nexus production tuning for 4 vCPU / 16 GiB host
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
innodb_buffer_pool_size = 4096M
innodb_log_file_size = 512M
max_connections = 200
innodb_read_io_threads = 8
innodb_write_io_threads = 8
table_open_cache = 4000
tmp_table_size = 128M
max_heap_table_size = 128M
[client]
default-character-set = utf8mb4
+5 -5
View File
@@ -1,5 +1,5 @@
# Host profile: 1 vCPU / 4 GiB
NEXUS_MEM_LIMIT=1g
NEXUS_CPUS=1
NEXUS_DB_POOL_SIZE=15
NEXUS_DB_MAX_OVERFLOW=10
# Host profile: 1 vCPU / 4 GiB
NEXUS_MEM_LIMIT=1g
NEXUS_CPUS=1
NEXUS_DB_POOL_SIZE=15
NEXUS_DB_MAX_OVERFLOW=10
+5 -5
View File
@@ -1,5 +1,5 @@
# Host profile: 2 vCPU / 8 GiB — default production
NEXUS_MEM_LIMIT=2g
NEXUS_CPUS=1.5
NEXUS_DB_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20
# Host profile: 2 vCPU / 8 GiB — default production
NEXUS_MEM_LIMIT=2g
NEXUS_CPUS=1.5
NEXUS_DB_POOL_SIZE=30
NEXUS_DB_MAX_OVERFLOW=20
+5 -5
View File
@@ -1,5 +1,5 @@
# Host profile: 4 vCPU / 16 GiB
NEXUS_MEM_LIMIT=4g
NEXUS_CPUS=3
NEXUS_DB_POOL_SIZE=50
NEXUS_DB_MAX_OVERFLOW=30
# Host profile: 4 vCPU / 16 GiB
NEXUS_MEM_LIMIT=4g
NEXUS_CPUS=3
NEXUS_DB_POOL_SIZE=50
NEXUS_DB_MAX_OVERFLOW=30
-733
View File
@@ -1,733 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Nexus 6.0 — 功能说明书</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6}
a{color:#60a5fa;text-decoration:none}
a:hover{text-decoration:underline}
/* Layout */
.wrap{max-width:960px;margin:0 auto;padding:32px 24px 80px}
/* Header */
.hero{text-align:center;padding:48px 0 40px;border-bottom:1px solid #1e293b;margin-bottom:40px}
.hero h1{font-size:2.2rem;font-weight:700;color:#f8fafc;letter-spacing:-0.02em}
.hero .sub{font-size:1rem;color:#64748b;margin-top:8px}
.badge{display:inline-block;margin:4px;padding:3px 10px;border-radius:999px;font-size:0.72rem;font-weight:600}
.badge-blue{background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb55}
.badge-green{background:#14371f;color:#4ade80;border:1px solid #16a34a55}
.badge-amber{background:#3d2a00;color:#fbbf24;border:1px solid #d9770655}
.badge-red{background:#3b1212;color:#f87171;border:1px solid #dc262655}
/* TOC */
.toc{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px 24px;margin-bottom:36px}
.toc h2{font-size:0.85rem;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;margin-bottom:12px}
.toc ol{padding-left:18px;column-count:2;column-gap:24px}
.toc li{margin:5px 0;font-size:0.88rem}
/* Section */
.section{margin-bottom:48px}
.section-title{font-size:1.4rem;font-weight:700;color:#f1f5f9;padding-bottom:10px;border-bottom:2px solid #334155;margin-bottom:24px;display:flex;align-items:center;gap:10px}
.section-title .icon{font-size:1.2rem}
/* Card */
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;margin-bottom:16px}
.card-title{font-size:1.05rem;font-weight:600;color:#f1f5f9;margin-bottom:6px;display:flex;align-items:center}
.card-title .num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:#334155;font-size:0.7rem;font-weight:700;color:#94a3b8;margin-right:8px;shrink:0}
.card-desc{color:#94a3b8;font-size:0.88rem;margin-bottom:14px}
/* Info rows */
.info-grid{display:grid;grid-template-columns:1fr;gap:10px}
.info-block{border-radius:8px;padding:12px 14px}
.info-block.usage{background:#0f2d1f;border:1px solid #166534}
.info-block.warn{background:#2a1a00;border:1px solid #92400e}
.info-block.note{background:#1a1f2e;border:1px solid #4338ca}
.info-title{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
.info-title.u{color:#4ade80}
.info-title.w{color:#fbbf24}
.info-title.n{color:#818cf8}
.info-block ul{padding-left:16px;font-size:0.86rem;color:#cbd5e1}
.info-block li{margin:4px 0}
.info-block p{font-size:0.86rem;color:#cbd5e1}
/* Sub table */
.feat-table{width:100%;border-collapse:collapse;font-size:0.86rem;margin-top:10px}
.feat-table th{text-align:left;color:#64748b;font-weight:600;font-size:0.75rem;text-transform:uppercase;padding:6px 10px;border-bottom:1px solid #334155}
.feat-table td{padding:7px 10px;border-bottom:1px solid #1e293b;color:#cbd5e1;vertical-align:top}
.feat-table tr:last-child td{border-bottom:none}
/* Footer */
footer{text-align:center;color:#334155;font-size:0.8rem;margin-top:60px;padding-top:24px;border-top:1px solid #1e293b}
code{background:#334155;color:#7dd3fc;padding:2px 6px;border-radius:4px;font-size:0.83em;font-family:monospace}
</style>
</head>
<body>
<div class="wrap">
<!-- HERO -->
<div class="hero">
<h1>⚡ Nexus 6.0</h1>
<p class="sub">服务器运维管理平台 — 全站功能说明书 · 2026-05-23</p>
<p class="sub" style="margin-top:10px;font-size:0.85rem">
Markdown 维护版:<code>docs/project/nexus-full-site-features.md</code> ·
AI 接续:<code>docs/project/AI-HANDOFF-2026-05-23.md</code>
</p>
<div style="margin-top:14px">
<span class="badge badge-blue">FastAPI</span>
<span class="badge badge-blue">SQLAlchemy 2.0</span>
<span class="badge badge-blue">Redis 7</span>
<span class="badge badge-blue">asyncssh</span>
<span class="badge badge-blue">Tailwind v4</span>
<span class="badge badge-green">2000+ 服务器</span>
<span class="badge badge-green">JWT + TOTP</span>
</div>
</div>
<!-- TOC -->
<div class="toc">
<h2>目录</h2>
<ol>
<li><a href="#sitemap">全站页面地图</a></li>
<li><a href="#network">网络与通道</a></li>
<li><a href="#auth">认证与访问控制</a></li>
<li><a href="#dashboard">仪表盘</a></li>
<li><a href="#alerts">告警中心</a></li>
<li><a href="#servers">服务器管理</a></li>
<li><a href="#assets">资产管理</a></li>
<li><a href="#webssh">Web SSH</a></li>
<li><a href="#commands">命令与推送日志</a></li>
<li><a href="#push">批量文件推送</a></li>
<li><a href="#scripts">脚本与执行历史</a></li>
<li><a href="#files">远程文件管理</a></li>
<li><a href="#schedules">定时调度</a></li>
<li><a href="#credentials">凭据管理</a></li>
<li><a href="#retries">重试队列</a></li>
<li><a href="#search">全局搜索</a></li>
<li><a href="#settings">系统设置</a></li>
<li><a href="#audit">审计日志</a></li>
<li><a href="#install">安装向导</a></li>
<li><a href="#guardian">3 层守护</a></li>
<li><a href="#agent">子机 Agent</a></li>
<li><a href="#background">后台任务</a></li>
<li><a href="#acceptance">开发验收标准</a></li>
<li><a href="#notimpl">未实现 / 已否决</a></li>
</ol>
</div>
<!-- 0. SITEMAP -->
<div class="section" id="sitemap">
<div class="section-title"><span class="icon">🗺</span> 全站页面地图(18 页)</div>
<div class="card">
<table class="feat-table">
<tr><th>页面</th><th>路径</th><th>功能</th></tr>
<tr><td>安装向导</td><td><code>/app/install.html</code></td><td>无 .env 时安装中心机</td></tr>
<tr><td>登录</td><td><code>/app/login.html</code></td><td>JWT + TOTP + 防暴破</td></tr>
<tr><td>仪表盘</td><td><code>/app/index.html</code></td><td>统计、WS 刷新统计</td></tr>
<tr><td>服务器</td><td><code>/app/servers.html</code></td><td>CRUD、Agent 安装/升级、漂移徽章</td></tr>
<tr><td>资产管理</td><td><code>/app/assets.html</code></td><td>Platform / Node</td></tr>
<tr><td>文件管理</td><td><code>/app/files.html</code></td><td>SSH 浏览与文件操作</td></tr>
<tr><td>推送</td><td><code>/app/push.html</code></td><td>rsync 批量 + dry-run</td></tr>
<tr><td>脚本库</td><td><code>/app/scripts.html</code></td><td>脚本 CRUD、直接执行</td></tr>
<tr><td>执行历史</td><td><code>/app/script-executions.html</code></td><td>停止/重试/日志</td></tr>
<tr><td>凭据</td><td><code>/app/credentials.html</code></td><td>DB 凭据 + 密码预设</td></tr>
<tr><td>定时调度</td><td><code>/app/schedules.html</code></td><td>Cron 推送或脚本</td></tr>
<tr><td>重试队列</td><td><code>/app/retries.html</code></td><td>推送失败重试</td></tr>
<tr><td>命令日志</td><td><code>/app/commands.html</code></td><td>WebSSH 命令 + 推送日志</td></tr>
<tr><td>告警中心</td><td><code>/app/alerts.html</code></td><td>告警历史与统计</td></tr>
<tr><td>审计</td><td><code>/app/audit.html</code></td><td>操作审计、多维过滤</td></tr>
<tr><td>设置</td><td><code>/app/settings.html</code></td><td>阈值、Telegram、白名单、TOTP</td></tr>
<tr><td>WebSSH</td><td><code>/app/terminal.html</code></td><td>xterm 终端</td></tr>
</table>
<p class="card-desc" style="margin-top:12px">共享:<code>layout.js</code>(侧栏/移动适配)、<code>api.js</code>JWT、toast、全局搜索)</p>
</div>
</div>
<!-- NETWORK -->
<div class="section" id="network">
<div class="section-title"><span class="icon">🔌</span> 网络与通道模型</div>
<div class="card">
<table class="feat-table">
<tr><th>方向</th><th>端口</th><th>用途</th><th>必须</th></tr>
<tr><td>子机 → 中心</td><td><strong>443 出站</strong></td><td>Agent 心跳、长任务 curl 回调</td><td></td></tr>
<tr><td>中心 → 子机</td><td><strong>22 入站</strong></td><td>推送、文件、脚本、健康检查、Agent 安装</td><td></td></tr>
<tr><td>子机本机</td><td><code>127.0.0.1:8601</code></td><td>Agent HTTP(可不暴露公网)</td><td>可选</td></tr>
</table>
<div class="info-block note" style="margin-top:12px"><div class="info-title n">说明</div><p>脚本执行与手动健康检查已改走 <strong>SSH</strong>,不依赖公网 8601。宝塔环境请用系统 Python 3.12 + <code>install.sh</code> venv,勿用面板 Python 项目管理器托管 Agent。</p></div>
</div>
</div>
<!-- 1. AUTH -->
<div class="section" id="auth">
<div class="section-title"><span class="icon">🔒</span> 1. 认证与访问控制</div>
<div class="card">
<div class="card-title"><span class="num">1</span> JWT 登录 + 自动刷新</div>
<p class="card-desc">管理员通过用户名+密码登录,获取 Access Token30 分钟)和 Refresh Token(7 天)。前端到期前 2 分钟自动换新 Token,无感续期。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>访问 <code>/app/login.html</code>,输入管理员账号密码</li>
<li>Token 存于 <code>localStorage</code>,关闭浏览器后 7 天内免登录</li>
<li>8 小时无操作自动退出</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>JWT 存于 localStorageXSS 漏洞可能导致 Token 泄露(已做 CSP 缓解)</li>
<li>无 RBAC:只有一个管理员角色,鉴权即信任</li>
<li>不支持多管理员角色区分</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> TOTP 双因素认证</div>
<p class="card-desc">可选启用 Google Authenticator 兼容的 TOTP。登录时先验密码,再验 6 位动态码(30 秒窗口)。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>进入「设置」→「安全设置」→ 启用 / <strong>重新绑定</strong> TOTP,扫描二维码</li>
<li>禁用 TOTP 需同时提供当前密码 + TOTP 码</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>TOTP 秘钥丢失无法恢复,需提前备份</li>
<li>系统时间偏差 &gt;30s 会导致验证失败(接受 ±1 窗口共 90s)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 登录防暴破</div>
<p class="card-desc">同一 IP + 用户名 15 分钟内失败 5 次,账号自动锁定并返回 HTTP 429。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>锁定基于 IP + 用户名组合,更换 IP 可绕过。适合防自动化爆破,不防分布式攻击。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
<p class="card-desc">可在设置中开启「仅允许白名单 IP 登录」。支持手动 IP/CIDR,以及从<strong>代理订阅 URL</strong>解析节点 IP(每 2 小时自动刷新,与手动列表合并)。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「设置」→「登录 IP 白名单」→ 总开关 + 订阅地址 + 手动 IP</li>
<li>非白名单 IP 登录被拒绝并记审计</li>
</ul></div>
</div>
</div>
<!-- 2. DASHBOARD -->
<div class="section" id="dashboard">
<div class="section-title"><span class="icon">📊</span> 2. 仪表盘与实时告警</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 实时统计面板</div>
<p class="card-desc">首页展示服务器总数、在线数、离线数、告警中台数、最近推送记录。数据来源 Redis(实时)+ MySQL(历史),通过 WebSocket 触发刷新。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>在线状态取自 Redis <code>heartbeat:{id}</code>,子机 60s 上报一次</li>
<li>告警计数 = Redis <code>alerts:*</code> key 数量</li>
<li>WS 断线会自动重连(3 次指数退避)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>在线状态最多滞后 60s(子机心跳间隔)</li>
<li>Redis 不可用时统计退化为 MySQL 数据,可能不准</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 告警通知(Telegram</div>
<p class="card-desc">CPU / 内存 / 磁盘超过阈值(默认 80%)时,通过 Telegram Bot 推送告警;恢复后发送恢复通知。同服务器同指标 5 分钟内不重复推送。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「设置」配置 <code>TELEGRAM_BOT_TOKEN</code> + <code>TELEGRAM_CHAT_ID</code></li>
<li>阈值可在设置页调整(CPU/内存/磁盘各独立)</li>
<li>浏览器仪表盘<strong>只显示计数,不弹窗、不响铃</strong>(设计决策)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>浏览器仪表盘不弹告警列表;历史见「告警中心」页</li>
<li>Telegram Bot 被封或网络不通时告警静默失败(有日志)</li>
<li>告警防抖 key 存于进程内存,重启后防抖状态重置</li>
</ul></div>
</div>
</div>
</div>
<!-- ALERTS -->
<div class="section" id="alerts">
<div class="section-title"><span class="icon">🔔</span> 告警中心</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 告警历史(alerts.html</div>
<p class="card-desc">将 CPU/内存/磁盘告警与恢复事件持久化到 <code>alert_logs</code>,支持分页、按服务器/类型/日期筛选,以及近 7 日统计图表数据。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>侧栏「告警中心」查看历史列表</li>
<li>API<code>GET /api/alert-history/</code><code>GET /api/alert-history/stats</code></li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 分项 Telegram 开关</div>
<p class="card-desc">设置页可单独关闭某类告警的 Telegram 推送(告警/恢复/系统/时钟漂移等),便于后续扩展通道。</p>
</div>
</div>
<!-- 3. SERVERS -->
<div class="section" id="servers">
<div class="section-title"><span class="icon">🖥</span> 3. 服务器管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 服务器 CRUD</div>
<p class="card-desc">增删改查服务器配置,支持 SSH 密钥 / 密码双认证,可按平台类型(Platform)和节点(Node 树)分组。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>新增服务器:填写名称、域名/IP、SSH 端口、认证方式</li>
<li>认证方式选「密钥」时粘贴私钥内容,自动 Fernet 加密存储</li>
<li>支持 <code>platform_id</code> / <code>node_id</code> 分组归类</li>
<li>服务器列表分页显示(默认 50 条/页)</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>批量导入(CSV)未实现,2000 台需逐一或通过 API 批量添加</li>
<li>删除服务器同时删除关联推送日志(CASCADE)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 健康检查</div>
<p class="card-desc">手动触发 <code>POST /api/servers/check</code>:经 <strong>SSH</strong> 执行 Python 采集 CPU/内存/磁盘(<code>remote_probe</code>),<strong>不需要</strong>公网 8601。</p>
<div class="info-block note"><div class="info-title n">与在线状态区别</div><p>列表「在线」主要来自 Agent <strong>443 心跳</strong>;健康检查用于即时 SSH 探活与指标快照。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> Agent 安装与升级</div>
<p class="card-desc">服务器详情「Agent 安装」:复制 <code>install.sh</code> 命令或一键 SSH 安装(带日志);安装后轮询在线。升级通过 SSH 拉取最新 <code>agent.py</code> 并 restart。</p>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>安装脚本:Python 3.12 venv、监听 <code>127.0.0.1:8601</code>,无需公网放行 8601</li>
<li>时钟漂移:心跳带 <code>agent_time</code>,列表显示 warn/crit 徽章</li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 每服务器 API Key</div>
<p class="card-desc">为每台服务器单独生成 Agent API Key(格式 <code>nxs-xxx</code>),用于该服务器子机与中心通信的专属认证。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>服务器详情 → 「生成 Agent Key」,完整 Key 仅本次显示,立即复制到子机 <code>config.json</code></li>
<li>列表页只显示前 8 位用于识别</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>Key 生成后无法再次查看(一次性展示设计)</li>
<li>丢失需重新生成并更新子机配置</li>
</ul></div>
</div>
</div>
</div>
<!-- ASSETS -->
<div class="section" id="assets">
<div class="section-title"><span class="icon">🗂</span> 资产管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Platform 与 Node</div>
<p class="card-desc"><code>assets.html</code> 管理资产类型模板(Platform)与树形节点(Node),服务器表单可关联分组。</p>
<p class="card-desc">API<code>/api/assets/platforms</code><code>/api/assets/nodes</code>(含 tree</p>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> SSH 会话与命令(只读查询)</div>
<p class="card-desc">同页或「命令日志」可查询 <code>ssh_sessions</code><code>command_logs</code>WebSSH 产生)。</p>
</div>
</div>
<!-- 4. WEB SSH -->
<div class="section" id="webssh">
<div class="section-title"><span class="icon">💻</span> 4. Web SSH 终端</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 浏览器终端</div>
<p class="card-desc">基于 xterm.js + asyncssh,在浏览器内打开远程服务器 SSH 会话,支持全屏、自动 resize、彩色输出。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>服务器列表点击「终端」→ 弹出 <code>/app/terminal.html?server_id=X</code></li>
<li>认证:先获取短效 webssh-token(15 分钟),绑定到该服务器,再建 WebSocket</li>
<li>支持窗口 resize 自动通知远程 shell</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>Token 通过 URL 参数传递(?token=...),HTTPS 下安全;HTTP 下有风险</li>
<li>不支持文件上传 / 拖拽(需用文件管理页)</li>
<li>会话不录像(已明确否决)</li>
<li>网络中断 WebSocket 不会自动重连,需刷新页面</li>
</ul></div>
</div>
</div>
</div>
<!-- COMMANDS -->
<div class="section" id="commands">
<div class="section-title"><span class="icon">💻</span> 命令与推送日志</div>
<div class="card">
<div class="card-title"><span class="num">1</span> WebSSH 会话与命令</div>
<p class="card-desc"><code>commands.html</code>:会话列表、命令列表(危险命令高亮)、按服务器过滤。数据来自 WebSSH,直接 SSH 不记录。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 推送日志</div>
<p class="card-desc">同页「推送日志」Tab:rsync 批量推送历史,支持服务器/状态/同步模式/触发方式过滤与分页。</p>
</div>
</div>
<!-- 5. PUSH -->
<div class="section" id="push">
<div class="section-title"><span class="icon">📤</span> 5. 批量文件推送</div>
<div class="card">
<div class="card-title"><span class="num">1</span> rsync 批量推送</div>
<p class="card-desc">将源目录通过 rsync-over-SSH 推送到选定的服务器列表,支持 4 种同步模式,内置分批(50 台/批)并发(10 路)控制。</p>
<table class="feat-table">
<tr><th>同步模式</th><th>rsync 参数</th><th>适用场景</th></tr>
<tr><td>增量同步</td><td><code>-az</code></td><td>日常更新,只同步有变动的文件</td></tr>
<tr><td>全量同步</td><td><code>-az --delete</code></td><td>强制目标与源完全一致,会删除目标多余文件</td></tr>
<tr><td>校验和</td><td><code>-az --checksum</code></td><td>按文件内容而非时间戳判断差异</td></tr>
<tr><td>直接覆盖</td><td><code>-az --inplace</code></td><td>不用临时文件,直接覆盖(大文件节省空间)</td></tr>
</table>
<div class="info-grid" style="margin-top:12px">
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li><strong>全量同步(--delete)会不可逆删除</strong>目标目录多余文件,务必先「预览」</li>
<li>rsync 在中心机运行,中心机需能 SSH 到所有子机</li>
<li>失败的服务器自动加入重试队列(指数退避,最多 100 次)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 推送前预览(dry-run</div>
<p class="card-desc">选中全量/覆盖模式时,页面显示橙色警告,引导先对首台服务器运行 <code>rsync --dry-run --stats</code>,查看待传输/删除文件数量后再决定是否推送。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>选择「全量同步」→ 出现橙色预览按钮 → 点击 → 展示首台服务器变动摘要</li>
<li>可展开「详细文件列表」(最多 200 行)</li>
<li>预览不阻塞推送,看完数据自行决定是否点「开始推送」</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>仅预览第一台选中的服务器,其余服务器可能略有差异</li>
<li>预览需 SSH 连通,与真实推送消耗相同时间(通常 &lt;5s</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> 失败自动重试队列</div>
<p class="card-desc">推送失败的服务器自动进入 <code>push_retry_jobs</code> 队列,后台每 5 分钟按指数退避重试(60s → 120s → 240s...,最大 64 分钟)。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>最多重试 100 次。可在「重试」页手动强制立即重试或删除任务。</p></div>
</div>
</div>
<!-- 6. SCRIPTS -->
<div class="section" id="scripts">
<div class="section-title"><span class="icon">📜</span> 脚本与执行历史</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 脚本库管理</div>
<p class="card-desc">维护可复用 Shell 脚本库(按 ops/deploy/check/cleanup 分类),一键下发到选定的服务器。</p>
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「脚本」页 → 新建脚本 → 填写名称/类别/内容</li>
<li>点击脚本名 → 编辑;点击「执行」→ 选择服务器 → 运行</li>
<li>支持直接输入临时命令(无需保存到库)</li>
</ul></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 批量执行(短任务 / 长任务)</div>
<p class="card-desc">短任务:同步等待结果(默认 30s 超时)。长任务:nohup 后台运行,完成后 curl 回调中心记录结果。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>选择服务器 → 填写命令 → 「执行」</li>
<li>执行走 <strong>SSH 22</strong>(结果含 <code>channel: ssh</code>),无需子机公网 8601</li>
<li>长任务:nohup + 子机 curl 回调中心(需 HTTPS <code>NEXUS_API_BASE_URL</code></li>
<li>「执行历史」页:停止 / 重试 / 标记卡住 / 拉取 nexus-job 日志</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li><code>rm -rf /</code> 等危险命令被中心<strong>拦截(400</strong>,但仅匹配预设规则</li>
<li>长任务依赖子机能访问中心 <code>NEXUS_API_BASE_URL</code>(必须配置且使用 HTTPS</li>
<li>执行历史默认保留 2 小时 Redis 热数据,60s 刷入 MySQL</li>
<li>执行命令时密码等敏感 $DB_* 变量存储时已脱敏(替换为 ***)</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> $DB_* 数据库凭据替换</div>
<p class="card-desc">脚本中使用 <code>$DB_USER</code> / <code>$DB_PASS</code> / <code>$DB_HOST</code> / <code>$DB_PORT</code> / <code>$DB_NAME</code> 占位符,执行时自动替换为选定的数据库凭据,密码不出现在脚本文本或日志中。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>替换在中心机完成后发往子机,子机接收到的已是明文命令;传输通过 HTTPS,子机侧无加密保护。</p></div>
</div>
</div>
<!-- 7. FILES -->
<div class="section" id="files">
<div class="section-title"><span class="icon">📁</span> 7. 远程文件管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 目录浏览</div>
<p class="card-desc">通过 SSH exec<code>ls -la</code>)浏览指定服务器的远程目录,支持面包屑导航和目录点击进入。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「文件管理」→ 选服务器 → 输入路径 → 「浏览」(或按 Enter)</li>
<li>点击目录名进入子目录,点击「..」返回上级</li>
<li>文件按 目录优先 / 字母顺序 排列</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>无左侧树状面板(每次需手动导航)</li>
<li>显示的文件大小为 <code>ls</code> 输出的字节数,大文件不友好</li>
</ul></div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 文件操作(删除/重命名/新建目录)</div>
<p class="card-desc">鼠标悬停文件/目录行显示操作按钮,通过 SSH exec 在远程执行操作。</p>
<table class="feat-table">
<tr><th>操作</th><th>命令</th><th>触发方式</th></tr>
<tr><td>删除文件</td><td><code>rm -f path</code></td><td>悬停 → 🗑 → confirm 弹窗</td></tr>
<tr><td>删除目录</td><td><code>rm -rf path</code></td><td>悬停 → 🗑 → 额外警告弹窗</td></tr>
<tr><td>重命名</td><td><code>mv src dst</code></td><td>悬停 → ✏ → prompt 输入新名</td></tr>
<tr><td>新建目录</td><td><code>mkdir -p path</code></td><td>右上角 📁+ → prompt 输入名</td></tr>
</table>
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">缺点 / 注意</div><ul>
<li>删除目录(rm -rf<strong>不可恢复</strong>,浏览器弹窗确认无法替代真正的安全保障</li>
<li>无文件编辑器(已明确否决,用 WebSSH vim/nano 代替)</li>
<li>无文件上传功能(可用 SFTP API 或 WebSSH scp</li>
<li>无文件内容预览</li>
</ul></div>
</div>
</div>
<!-- 8. SCHEDULES -->
<div class="section" id="schedules">
<div class="section-title"><span class="icon"></span> 8. 定时调度</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Cron 调度(推送 + 脚本)</div>
<p class="card-desc">支持两种 <code>schedule_type</code><strong>文件推送</strong><strong>脚本执行</strong>。5 字段 Cron,后台每 60s 检查(仅 Primary Worker)。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「定时」→ 选类型 → 填 Cron、目标服务器</li>
<li>脚本型:选脚本库 ID 或内联内容,可设超时与长任务</li>
<li>示例:<code>0 2 * * *</code> = 每天凌晨 2 点</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>Cron 精度受 60s 检查间隔限制</li>
<li>脚本调度同样走 SSH 执行通道</li>
</ul></div>
</div>
</div>
</div>
<!-- RETRIES -->
<div class="section" id="retries">
<div class="section-title"><span class="icon">🔄</span> 重试队列</div>
<div class="card">
<p class="card-desc"><code>retries.html</code> 管理推送失败任务:<code>retry_runner</code> 每 5 分钟指数退避;可手动立即重试或删除。</p>
</div>
</div>
<!-- SEARCH -->
<div class="section" id="search">
<div class="section-title"><span class="icon">🔍</span> 全局搜索</div>
<div class="card">
<p class="card-desc">侧栏搜索框调用 <code>GET /api/search/</code>,跨服务器、脚本、凭据、定时任务关键字匹配。</p>
</div>
</div>
<!-- 9. CREDENTIALS -->
<div class="section" id="credentials">
<div class="section-title"><span class="icon">🔑</span> 9. 凭据管理</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 数据库凭据</div>
<p class="card-desc">存储 MySQL/PostgreSQL 连接信息,密码 Fernet 加密,API 响应不含密码(仅元数据)。供脚本执行时通过 $DB_* 变量引用。</p>
<div class="info-block warn"><div class="info-title w">注意</div><p>凭据只加密存储,子机接收到的命令已含明文连接信息(仅在中心→子机的 HTTPS 传输中受保护)。</p></div>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> 密码预设</div>
<p class="card-desc">存储 SSH 密码模板(Fernet 加密),可在推送配置中引用,避免重复输入常用密码。</p>
<div class="info-block note"><div class="info-title n">注意</div><p>密码预设不能用于脚本 $DB_PASS 替换,只用于推送密码模板。</p></div>
</div>
</div>
<!-- 10. SETTINGS -->
<div class="section" id="settings">
<div class="section-title"><span class="icon">⚙️</span> 10. 系统设置</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 可配置项</div>
<table class="feat-table">
<tr><th>设置项</th><th>说明</th><th>是否可改</th></tr>
<tr><td>系统名称 / 标题</td><td>品牌展示名称</td><td>✅ 可改</td></tr>
<tr><td>告警阈值(CPU/内存/磁盘)</td><td>超过阈值触发 Telegram 告警,默认 80%</td><td>✅ 可改</td></tr>
<tr><td>Redis URL</td><td>Redis 连接地址</td><td>✅ 可改</td></tr>
<tr><td>数据库连接池大小</td><td>pool_size / max_overflow</td><td>✅ 可改</td></tr>
<tr><td>Telegram Bot Token / Chat ID</td><td>告警推送配置</td><td>✅ 可改</td></tr>
<tr><td>SECRET_KEY / API_KEY / ENCRYPTION_KEY</td><td>核心安全密钥</td><td>❌ 不可改(需改 .env 重启)</td></tr>
<tr><td>DATABASE_URL</td><td>数据库连接</td><td>❌ 不可改</td></tr>
</table>
</div>
<div class="card">
<div class="card-title"><span class="num">2</span> Telegram</div>
<p class="card-desc">配置 Bot Token / Chat ID<strong>测试发送</strong>;通过 getUpdates <strong>检测 Chat ID</strong>;各告警类型独立开关。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">3</span> TOTP 与 API Key</div>
<p class="card-desc">TOTP 启用/禁用/<strong>重新绑定</strong>;全局 API Key 需密码二次验证后查看(记审计)。</p>
</div>
<div class="card">
<div class="card-title"><span class="num">4</span> 登录 IP 白名单</div>
<p class="card-desc">总开关、订阅 URL(2h 刷新)、手动 IP、解析预览(见「认证」章节)。</p>
</div>
</div>
<!-- 11. AUDIT -->
<div class="section" id="audit">
<div class="section-title"><span class="icon">📋</span> 11. 审计日志</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 操作审计</div>
<p class="card-desc">所有 CUD 操作(创建/更新/删除服务器、推送、执行脚本、登录、改密码等)自动写入 <code>audit_logs</code>,记录操作人、IP、目标、详情。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>「审计」页:日期起止、管理员用户名、操作类型(分组下拉)</li>
<li>每页 50 条分页</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">缺点 / 注意</div><ul>
<li>无日志导出(已否决 CSV/JSON</li>
<li>审计日志不会自动清理,长期需归档</li>
</ul></div>
</div>
</div>
</div>
<!-- 12. INSTALL -->
<div class="section" id="install">
<div class="section-title"><span class="icon">🚀</span> 12. 安装向导</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 5 步安装向导</div>
<p class="card-desc"><code>.env</code> 文件时系统进入安装模式,访问 <code>/app/install.html</code> 完成 5 步配置:欢迎 → 数据库 → Redis → 管理员账号 → 完成。自动生成 API Key / Secret Key / Encryption Key。</p>
<div class="info-grid">
<div class="info-block usage"><div class="info-title u">使用说明</div><ul>
<li>安装完成后 <code>.env</code> 自动生成,安装 API 除 GET /status 外全部锁定(返回 403</li>
<li>重装需手动删除 <code>.env</code> 并重启</li>
</ul></div>
<div class="info-block warn"><div class="info-title w">注意</div><ul>
<li>安装过程未验证数据库/Redis 连接成功才允许进入下一步(应在步骤中手动验证)</li>
<li>安装模式下除 install API 和静态文件外,其余 API 返回 503</li>
</ul></div>
</div>
</div>
</div>
<!-- 13. GUARDIAN -->
<div class="section" id="guardian">
<div class="section-title"><span class="icon">🛡</span> 13. 三层守护机制</div>
<div class="card">
<div class="card-title"><span class="num">1</span> 三层守护</div>
<table class="feat-table">
<tr><th>层级</th><th>机制</th><th>触发条件</th><th>动作</th></tr>
<tr><td>Layer 1</td><td>Supervisor</td><td>Python 进程崩溃</td><td>自动重启</td></tr>
<tr><td>Layer 2</td><td>Python self_monitor30s</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
<tr><td>Layer 3</td><td>Shell cron1 分钟)</td><td>连续 3 次 /health 失败</td><td>supervisorctl restart + Telegram</td></tr>
</table>
<div class="info-block warn" style="margin-top:10px"><div class="info-title w">注意</div><p>Shell cron 依赖宝塔/系统 crontab 配置,部署时需手动添加定时任务。</p></div>
</div>
</div>
<!-- 14. AGENT -->
<div class="section" id="agent">
<div class="section-title"><span class="icon">🤖</span> 14. 子机 Agent</div>
<div class="card">
<div class="card-title"><span class="num">1</span> Agent 功能</div>
<p class="card-desc">部署在每台被管服务器上的轻量服务:<strong>心跳走 443 出站</strong>HTTP 仅本机 <code>127.0.0.1:8601</code>。中心侧脚本已改 <strong>SSH</strong>/exec 端点保留供兼容。</p>
<table class="feat-table">
<tr><th>能力</th><th>说明</th></tr>
<tr><td>心跳</td><td>POST 中心 <code>/api/agent/heartbeat</code>,写 Redis,智能加急</td></tr>
<tr><td>时钟漂移</td><td><code>agent_time</code> 与中心比对,warn/crit + Telegram</td></tr>
<tr><td>IP 白名单</td><td><code>allowed_ips</code> 限制访问本机 Agent HTTP</td></tr>
<tr><td>长任务回调</td><td>POST <code>/api/agent/script-callback</code></td></tr>
<tr><td>安装</td><td><code>web/agent/install.sh</code> — python3.12 venv + systemd</td></tr>
</table>
<div class="info-block warn" style="margin-top:12px"><div class="info-title w">注意</div><ul>
<li>升级 API 存在,建议与 venv 安装脚本对齐后使用</li>
<li>长任务回调需子机出站访问中心 HTTPS</li>
</ul></div>
</div>
</div>
<!-- BACKGROUND -->
<div class="section" id="background">
<div class="section-title"><span class="icon"></span> 后台任务(Primary Worker</div>
<div class="card">
<table class="feat-table">
<tr><th>任务</th><th>间隔</th><th>说明</th></tr>
<tr><td>heartbeat_flush</td><td>10min</td><td>Redis 心跳 → MySQL</td></tr>
<tr><td>self_monitor</td><td>30s</td><td>Redis/MySQL/WS 自检 + Telegram</td></tr>
<tr><td>schedule_runner</td><td>60s</td><td>Cron 调度</td></tr>
<tr><td>retry_runner</td><td>5min</td><td>推送重试</td></tr>
<tr><td>ip_allowlist_refresh</td><td>2h</td><td>订阅 IP 刷新</td></tr>
<tr><td>script_execution_flush</td><td>60s</td><td>执行记录 Redis→MySQL</td></tr>
</table>
</div>
</div>
<!-- ACCEPTANCE -->
<div class="section" id="acceptance">
<div class="section-title"><span class="icon"></span> 标准与验收(与团队同等严谨)</div>
<div class="card">
<p class="card-desc"><strong>全量标准转接</strong><code>docs/project/standards-transfer-package.md</code> — 含 Cursor 规则、<code>standards/</code> 三件套、逐行八步 DoD、设计/上线清单。下一任 AI <strong>不得降低</strong>执行力度。</p>
<table class="feat-table">
<tr><th>类别</th><th>权威路径</th></tr>
<tr><td>标准转接包</td><td><code>docs/project/standards-transfer-package.md</code></td></tr>
<tr><td>验收 L0L5</td><td><code>development-acceptance-standard.md</code></td></tr>
<tr><td>系统开发</td><td><code>standards/system-development-standard.md</code></td></tr>
<tr><td>逐行审计 v2</td><td><code>standards/line-walk-audit-standard-v2.md</code></td></tr>
<tr><td>审计原则</td><td><code>standards/audit-core-principles.md</code></td></tr>
<tr><td>Cursor</td><td><code>.cursor/rules/*.mdc</code>(含 audit-line-review</td></tr>
</table>
<p class="card-desc" style="margin-top:12px">逐行审查:8 步/文件、Closure 全表含 SAFE、FINDING 必须 <code>文件:行号</code>、单回复 ≤5 文件。AI 接续:HANDOFF + 全站功能 + 标准转接包。</p>
</div>
</div>
<!-- 15. NOT IMPL -->
<div class="section" id="notimpl">
<div class="section-title"><span class="icon">🚫</span> 15. 未实现 / 已否决功能</div>
<div class="card">
<table class="feat-table">
<tr><th>功能</th><th>状态</th><th>原因 / 替代方案</th></tr>
<tr><td>文件编辑器(Monaco/ACE</td><td><span class="badge badge-red">已否决</span></td><td>运维场景用 WebSSH + vim/nano;嵌入编辑器增加复杂度无实质收益</td></tr>
<tr><td>会话录像(asciicast</td><td><span class="badge badge-red">已否决</span></td><td>命令日志已满足审计需求</td></tr>
<tr><td>RBAC 权限体系</td><td><span class="badge badge-red">已否决</span></td><td>鉴权即信任,单管理员模型;增加 RBAC 代价远超收益</td></tr>
<tr><td>移动端适配</td><td><span class="badge badge-red">已否决</span></td><td>2000 台服务器运维不会用手机操作</td></tr>
<tr><td>批量服务器导入(CSV</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 API 批量 POST /api/servers/ 实现</td></tr>
<tr><td>文件上传到远程服务器</td><td><span class="badge badge-amber">未实现</span></td><td>可用 WebSSH scp / 推送功能代替</td></tr>
<tr><td>文件内容预览</td><td><span class="badge badge-amber">未实现</span></td><td>可通过 WebSSH cat 查看</td></tr>
<tr><td>WebSSH 断线自动重连</td><td><span class="badge badge-amber">未实现</span></td><td>当前需刷新页面;计划自动重试+按钮</td></tr>
<tr><td>仪表盘历史趋势图</td><td><span class="badge badge-amber">未实现</span></td><td>仅实时 Redis 指标</td></tr>
<tr><td>宝塔 API 监控集成</td><td><span class="badge badge-amber">未实现</span></td><td>可选;参考 btpanel-skills</td></tr>
<tr><td>Agent mTLS</td><td><span class="badge badge-amber">讨论中</span></td><td>未实现</td></tr>
<tr><td>首次部署 E2ET1T5、T-SE</td><td><span class="badge badge-amber">待验证</span></td><td>见 production-verification-checklist.md</td></tr>
</table>
</div>
</div>
<!-- FOOTER -->
<footer>
<p>Nexus 6.0 全站功能说明书 · 2026-05-23(对齐代码库)</p>
<p style="margin-top:6px">
<a href="project/nexus-full-site-features.md">全站功能</a> ·
<a href="project/standards-transfer-package.md">标准转接</a> ·
<a href="project/development-acceptance-standard.md">验收标准</a> ·
<a href="project/AI-HANDOFF-2026-05-23.md">AI 接续</a>
</p>
<p style="margin-top:4px">Python 3.12 / FastAPI 0.115 / MySQL 8.4 / Redis 7 / asyncssh 2.17 / Tailwind v4</p>
</footer>
</div>
</body>
</html>
@@ -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
```
@@ -1,21 +0,0 @@
# 2026-06-06 — Gitea 推送脚本与本地 secrets
## 摘要
新增 `scripts/git-push.sh`,从 `deploy/nexus-1panel.secrets.sh` 读取 admin 凭据推送;更新 `.cursorrules` / `linux-dev-paths.md`
## 动机
公共仓库可匿名 pull,push 需写权限;凭据仅存本机 gitignore 文件,不入仓库。
## 涉及文件
- `scripts/git-push.sh`
- `deploy/nexus-1panel.secrets.sh.example`
- `.cursorrules``docs/project/linux-dev-paths.md`
## 验证
```bash
bash scripts/git-push.sh main # 需已配置 deploy/nexus-1panel.secrets.sh
```
@@ -1,34 +0,0 @@
# 2026-06-06 — 安装脚本按机自动生成唯一密钥
## 摘要
Docker / 1Panel 全新安装路径统一通过 `scripts/generate_nexus_secrets.py` 生成每台服务器独立的 `MYSQL_*``NEXUS_SECRET_KEY` / `NEXUS_API_KEY` / `NEXUS_ENCRYPTION_KEY`,写入 `docker/.env.prod` 并备份到 `/root/.nexus-install-secrets-*.env`。安装向导 `init-db` 复用容器环境内预生成密钥,避免与 Compose 不一致。
## 动机
新机安装不应从 `nexus-1panel.secrets.sh` 复制生产密钥;此前 `--fresh` 留空密钥导致 Compose 校验失败或各机密钥相同。
## 涉及文件
- `scripts/generate_nexus_secrets.py`(新建)
- `deploy/nexus-1panel.sh``write_env_prod` / `--reuse-secrets` / `--fresh` 忽略 secrets 中 NEXUS 项
- `deploy/install-nexus-fresh.sh` / `deploy/quick-install.sh`
- `docker/generate_env.py` — 复用同一生成器
- `docker/entrypoint.sh``NEXUS_INSTALL_WIZARD_PENDING=1` 时不写 `/app/.env`
- `docker/docker-compose.prod.yml` — 传递 `NEXUS_INSTALL_WIZARD_PENDING`
- `server/api/install.py``_install_keys_from_environment()`
- `deploy/README-1panel.md` · `deploy/nexus-1panel.secrets.sh.example`
## 迁移 / 重启
已部署实例无需变更。新机安装后需完成 `/app/install.html` 向导。
## 验证
```bash
python3 scripts/generate_nexus_secrets.py write-prod \
--template docker/.env.prod.example --output /tmp/t.env.prod \
--domain test.example.com --profile-env docker/profiles/2c8g.env --fresh --wizard-pending
grep NEXUS_SECRET_KEY /tmp/t.env.prod
ruff check server/api/install.py scripts/generate_nexus_secrets.py
```
@@ -1,29 +0,0 @@
# Changelog — 生产 SSH 凭据纳入 secrets + deploy 自动加载
**日期**2026-06-07
## 摘要
本机生产 SSH 私钥路径写入 `deploy/nexus-1panel.secrets.sh`gitignore);`deploy-production.sh` 启动时自动 source,与 `git-push.sh` 同一凭据文件。
## 动机
本机 `Host nexus` 未指向生产 VPS;部署脚本需 `NEXUS_SSH` / `NEXUS_SSH_KEY`,避免每次手动 export。
## 涉及文件
- `deploy/deploy-production.sh`
- `deploy/nexus-1panel.secrets.sh.example`
- `docs/project/linux-dev-paths.md`
- `deploy/nexus-1panel.secrets.sh`(本地 only,不入 git
## 迁移 / 重启
无。
## 验证
```bash
source deploy/nexus-1panel.secrets.sh
ssh -i "$NEXUS_SSH_KEY" "$NEXUS_SSH" "echo SSH_OK"
```
@@ -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,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,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,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,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,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,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;部署脚本属主检测为向后兼容,可保留。
@@ -41,3 +41,4 @@
- 行按钮改 `small` + `tonal` + 字重 600,恢复「一键登录」全文
- 批量栏改 `flat` 实色按钮、分组标题加深,底卡用 surface 实底
- 行操作移除「删除」;删除请用批量栏「批量删除」
- 批量栏「已选 N 台」增加「选择」分组标题,与其他分区对齐
@@ -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
```
+26 -26
View File
@@ -1,26 +1,26 @@
# Nexus 文件管理 — 目标机免密 sudo 示例(须运维审阅后安装)
#
# 安装步骤(在目标服务器上):
# 1. 将本文件复制为 /etc/sudoers.d/nexus-files(权限 0440
# 2. 把下面 deploy 替换为 Nexus 里该服务器的 SSH 用户名
# 3. visudo -cf /etc/sudoers.d/nexus-files
# 4. 用该 SSH 用户执行:sudo -n true && sudo -n bash -c 'command -v chmod'
#
# 说明:
# - 后端在权限失败时会执行 sudo -n bash -c '…',因此必须允许 /bin/bash 或 /usr/bin/bash
# - 写入回退还会用到 tee、mv、mkdir、rm、cat 等
# - 禁止使用 ALL=(ALL) NOPASSWD: ALL
deploy ALL=(ALL) NOPASSWD: \
/bin/bash, /usr/bin/bash, \
/bin/chmod, /usr/bin/chmod, \
/bin/chown, /usr/bin/chown, \
/bin/cp, /usr/bin/cp, \
/bin/mv, /usr/bin/mv, \
/bin/mkdir, /usr/bin/mkdir, \
/bin/rm, /usr/bin/rm, \
/bin/tar, /usr/bin/tar, \
/usr/bin/unzip, /usr/bin/zip, \
/usr/bin/tee, /bin/cat, /usr/bin/cat, \
/usr/bin/touch, /bin/touch, \
/usr/bin/rsync, /bin/rsync
# Nexus 文件管理 — 目标机免密 sudo 示例(须运维审阅后安装)
#
# 安装步骤(在目标服务器上):
# 1. 将本文件复制为 /etc/sudoers.d/nexus-files(权限 0440
# 2. 把下面 deploy 替换为 Nexus 里该服务器的 SSH 用户名
# 3. visudo -cf /etc/sudoers.d/nexus-files
# 4. 用该 SSH 用户执行:sudo -n true && sudo -n bash -c 'command -v chmod'
#
# 说明:
# - 后端在权限失败时会执行 sudo -n bash -c '…',因此必须允许 /bin/bash 或 /usr/bin/bash
# - 写入回退还会用到 tee、mv、mkdir、rm、cat 等
# - 禁止使用 ALL=(ALL) NOPASSWD: ALL
deploy ALL=(ALL) NOPASSWD: \
/bin/bash, /usr/bin/bash, \
/bin/chmod, /usr/bin/chmod, \
/bin/chown, /usr/bin/chown, \
/bin/cp, /usr/bin/cp, \
/bin/mv, /usr/bin/mv, \
/bin/mkdir, /usr/bin/mkdir, \
/bin/rm, /usr/bin/rm, \
/bin/tar, /usr/bin/tar, \
/usr/bin/unzip, /usr/bin/zip, \
/usr/bin/tee, /bin/cat, /usr/bin/cat, \
/usr/bin/touch, /bin/touch, \
/usr/bin/rsync, /bin/rsync
@@ -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 注册。

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