Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8363746cf6 | |||
| b49dad8bda | |||
| 279259092d | |||
| 74149e0d90 | |||
| 1933f0af6e |
@@ -0,0 +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`
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"plugins": {
|
||||
"compound-engineering": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -38,11 +38,16 @@ FastAPI + Async SQLAlchemy + Redis · Vue 3 + Vuetify 4 SPA(15 页)· 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
@@ -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
|
||||
|
||||
@@ -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
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -19,6 +19,14 @@ cd frontend && npm run dev # :3000/app/
|
||||
|
||||
路径见 [linux-dev-paths.md](docs/project/linux-dev-paths.md)。
|
||||
|
||||
## Git(仅本机快照)
|
||||
|
||||
- 本地 `git commit` 用于回滚;**不 `git push`**(除非你明确要求)。
|
||||
- 已移除 `origin` 跟踪,避免误推;原远程:`http://66.154.115.8:3000/admin/Nexus.git`(需恢复时说一声)。
|
||||
- `.env`、`SECRETS.md`、`*.pem` 已在 `.gitignore`,**勿提交**。
|
||||
- 改代码仍写 `docs/changelog/YYYY-MM-DD-*.md`(≥10 行)。
|
||||
- 用户说「提交快照 / commit」时执行。
|
||||
|
||||
## 强制约束
|
||||
|
||||
- 完美实现:`.cursor/rules/perfect-implementation.mdc`
|
||||
@@ -32,13 +40,57 @@ 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` 当你明确要求时。
|
||||
|
||||
详情见功能指南 §17–18。
|
||||
|
||||
## 归档文档
|
||||
|
||||
历史审计合并卷:`docs/archive/README.md`
|
||||
|
||||
## 代码地图(Agent 先读这里,勿全库搜)
|
||||
|
||||
```
|
||||
server/main.py # 路由挂载入口
|
||||
server/config.py # 配置(.env + settings 表)
|
||||
server/api/*.py # HTTP/WebSocket 路由(按模块拆分)
|
||||
server/application/ # 业务 services
|
||||
server/infrastructure/ # DB / Redis / SSH / Telegram
|
||||
server/domain/models/ # SQLAlchemy ORM
|
||||
frontend/src/pages/ # Vue 页面(15 页 SPA)
|
||||
frontend/src/router/ # Hash 路由 #/
|
||||
frontend/src/api/ # 前端 API 客户端
|
||||
frontend/e2e/ # Playwright 验收
|
||||
tests/test_*.py # pytest
|
||||
docs/project/ # 功能/部署 SSOT
|
||||
deploy/ # 生产部署脚本
|
||||
scripts/ # 本地验证、MCP、门控
|
||||
.cursor/mcp.json # mysql-mcp
|
||||
```
|
||||
|
||||
## 问题 → 文件(常见锚点)
|
||||
|
||||
| 问什么 | 先看 |
|
||||
|--------|------|
|
||||
| 某 API 怎么实现 | `server/main.py` 找 router → `server/api/<模块>.py` |
|
||||
| 服务器列表/搜索/exec | `server/api/servers.py`, `server/api/scripts.py` |
|
||||
| 认证/JWT | `server/api/auth.py`, `server/api/dependencies.py` |
|
||||
| Agent 心跳/安装 | `server/api/agent.py` |
|
||||
| WebSSH/终端 | `server/api/webssh.py`, `server/api/terminal.py` |
|
||||
| 同步/文件/搜索 | `server/api/sync_v2.py`, `files.py`, `search.py` |
|
||||
| 设置/审计/告警 | `server/api/settings.py` |
|
||||
| 前端某页 UI | `frontend/src/router/index.ts` → `frontend/src/pages/` |
|
||||
| MySQL MCP | `.cursor/mcp.json`, `scripts/linux_mcp_mysql.sh` |
|
||||
| 本地开发路径 | `docs/project/linux-dev-paths.md` |
|
||||
| 14 页/API 全览 | `docs/project/nexus-functional-development-guide.md` §9–10 |
|
||||
|
||||
## 探索规则(省 token)
|
||||
|
||||
详见 `.cursor/rules/agent-exploration.mdc`。要点:**先地图再 Grep(带 path)→ Read ≤5 个文件 → 停止**。
|
||||
|
||||
+46
-33
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
FROM mcr.microsoft.com/playwright/python:v1.49.1-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends xvfb \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY browser_url_safe.py browser_stealth.py config.py session_manager.py main.py docker-entrypoint.sh ./
|
||||
RUN sed -i 's/\r$//' docker-entrypoint.sh && chmod +x docker-entrypoint.sh
|
||||
|
||||
ENV WORKER_BIND=0.0.0.0:8443
|
||||
ENV DISPLAY=:99
|
||||
|
||||
EXPOSE 8443
|
||||
|
||||
ENTRYPOINT ["/bin/bash", "docker-entrypoint.sh"]
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Reduce automation fingerprints for sites with CAPTCHA (e.g. 阿里云)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
CHROMIUM_ARGS = [
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-infobars",
|
||||
"--window-position=0,0",
|
||||
"--lang=zh-CN",
|
||||
]
|
||||
|
||||
# Playwright adds --enable-automation by default; drop it for CAPTCHA sites.
|
||||
IGNORE_DEFAULT_ARGS = ["--enable-automation"]
|
||||
|
||||
STEALTH_INIT_SCRIPT = """
|
||||
(() => {
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
|
||||
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en-US', 'en'] });
|
||||
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications'
|
||||
? Promise.resolve({ state: Notification.permission })
|
||||
: originalQuery(parameters)
|
||||
);
|
||||
window.chrome = window.chrome || { runtime: {} };
|
||||
})();
|
||||
"""
|
||||
|
||||
DEFAULT_USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
CONTEXT_OPTIONS = {
|
||||
"user_agent": DEFAULT_USER_AGENT,
|
||||
"locale": "zh-CN",
|
||||
"timezone_id": "Asia/Shanghai",
|
||||
"color_scheme": "light",
|
||||
"device_scale_factor": 1,
|
||||
"has_touch": False,
|
||||
"is_mobile": False,
|
||||
"java_script_enabled": True,
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"""SSRF-safe URL validation (Worker copy — keep in sync with server/utils/browser_url_safe.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
BLOCKED_HOSTNAMES = frozenset({
|
||||
"localhost",
|
||||
"localhost.localdomain",
|
||||
"metadata",
|
||||
"metadata.google.internal",
|
||||
"169.254.169.254",
|
||||
})
|
||||
|
||||
|
||||
def _blocked_ip(ip: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast:
|
||||
return True
|
||||
if isinstance(ip, ipaddress.IPv4Address):
|
||||
if ip in ipaddress.ip_network("0.0.0.0/8"):
|
||||
return True
|
||||
if ip in ipaddress.ip_network("100.64.0.0/10"):
|
||||
return True
|
||||
if isinstance(ip, ipaddress.IPv6Address):
|
||||
if ip == ipaddress.IPv6Address("::1"):
|
||||
return True
|
||||
if ip in ipaddress.ip_network("fc00::/7"):
|
||||
return True
|
||||
if ip in ipaddress.ip_network("fe80::/10"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resolve_host_blocked(hostname: str) -> str | None:
|
||||
host = hostname.lower().strip(".")
|
||||
if not host:
|
||||
return "Empty hostname"
|
||||
if host in BLOCKED_HOSTNAMES or "metadata" in host:
|
||||
return f"Blocked hostname: {hostname}"
|
||||
try:
|
||||
infos = socket.getaddrinfo(host, None, type=socket.SOCK_STREAM)
|
||||
except socket.gaierror as exc:
|
||||
return f"DNS resolution failed: {exc}"
|
||||
if not infos:
|
||||
return f"No addresses for hostname: {hostname}"
|
||||
for info in infos:
|
||||
ip_str = info[4][0]
|
||||
try:
|
||||
ip = ipaddress.ip_address(ip_str)
|
||||
except ValueError:
|
||||
return f"Invalid IP: {ip_str}"
|
||||
if _blocked_ip(ip):
|
||||
return f"Blocked IP: {ip_str}"
|
||||
return None
|
||||
|
||||
|
||||
def validate_navigation_url(url: str) -> tuple[str | None, str | None]:
|
||||
trimmed = url.strip()
|
||||
if not trimmed:
|
||||
return None, "Empty URL"
|
||||
try:
|
||||
with_proto = trimmed if "://" in trimmed else f"https://{trimmed}"
|
||||
parsed = urlparse(with_proto)
|
||||
except Exception:
|
||||
return None, "Invalid URL"
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return None, f"Unsupported scheme: {parsed.scheme}"
|
||||
if parsed.username or parsed.password:
|
||||
return None, "Credentials in URL are not allowed"
|
||||
if not parsed.hostname:
|
||||
return None, "Missing hostname"
|
||||
host_err = resolve_host_blocked(parsed.hostname)
|
||||
if host_err:
|
||||
return None, host_err
|
||||
return parsed.geturl(), None
|
||||
@@ -1,22 +0,0 @@
|
||||
"""Browser Worker configuration."""
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class WorkerSettings(BaseSettings):
|
||||
WORKER_SECRET: str = ""
|
||||
WORKER_BIND: str = "0.0.0.0:8443"
|
||||
WORKER_MAX_SESSIONS: int = 2
|
||||
WORKER_IDLE_TIMEOUT_SEC: int = 1800
|
||||
# POST /v1/sessions without WS attach — reclaim zombie reservations
|
||||
WORKER_RESERVE_TIMEOUT_SEC: int = 120
|
||||
# false + Xvfb — better for 阿里云等验证码;true 省资源
|
||||
WORKER_HEADLESS: str = "false"
|
||||
WORKER_SCREENCAST_QUALITY: int = 90
|
||||
WORKER_DEFAULT_VIEWPORT_W: int = 1280
|
||||
WORKER_DEFAULT_VIEWPORT_H: int = 720
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
|
||||
settings = WorkerSettings()
|
||||
@@ -1,11 +0,0 @@
|
||||
services:
|
||||
browser-worker:
|
||||
build: .
|
||||
container_name: nexus-browser-worker
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8443:8443"
|
||||
shm_size: "1gb"
|
||||
ipc: host
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Headed Chromium needs a virtual display (CAPTCHA / anti-bot).
|
||||
if [[ "${WORKER_HEADLESS:-false}" != "true" && "${WORKER_HEADLESS:-false}" != "1" ]]; then
|
||||
export DISPLAY="${DISPLAY:-:99}"
|
||||
if ! pgrep -x Xvfb >/dev/null 2>&1; then
|
||||
Xvfb "${DISPLAY}" -screen 0 1920x1080x24 -ac +extension GLX +render -noreset &
|
||||
sleep 1
|
||||
fi
|
||||
fi
|
||||
exec uvicorn main:app --host 0.0.0.0 --port 8443 --log-level info
|
||||
@@ -1,147 +0,0 @@
|
||||
"""Browser Worker — Playwright Chromium + WebSocket stream."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, Header, HTTPException, WebSocket, WebSocketDisconnect
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from config import settings
|
||||
from session_manager import manager
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("browser_worker")
|
||||
|
||||
|
||||
def _check_key(header: str | None) -> None:
|
||||
if not settings.WORKER_SECRET:
|
||||
raise HTTPException(status_code=503, detail="Worker secret not configured")
|
||||
if not header or header != settings.WORKER_SECRET:
|
||||
raise HTTPException(status_code=401, detail="Invalid worker key")
|
||||
|
||||
|
||||
class CreateSessionBody(BaseModel):
|
||||
session_id: str = Field(min_length=8, max_length=64)
|
||||
viewport_w: int = Field(default=1280, ge=320, le=1920)
|
||||
viewport_h: int = Field(default=720, ge=240, le=1080)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
await manager.start()
|
||||
logger.info("Playwright Chromium ready")
|
||||
yield
|
||||
await manager.stop()
|
||||
|
||||
|
||||
app = FastAPI(title="Nexus Browser Worker", lifespan=lifespan)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"sessions": manager.session_count(),
|
||||
"max_sessions": settings.WORKER_MAX_SESSIONS,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/v1/sessions")
|
||||
async def create_session(
|
||||
body: CreateSessionBody,
|
||||
x_nexus_worker_key: str | None = Header(default=None, alias="X-Nexus-Worker-Key"),
|
||||
):
|
||||
_check_key(x_nexus_worker_key)
|
||||
try:
|
||||
await manager.reserve_session(body.session_id, body.viewport_w, body.viewport_h)
|
||||
except ValueError as exc:
|
||||
code = str(exc)
|
||||
if code == "max_sessions":
|
||||
raise HTTPException(status_code=503, detail="Max sessions reached") from exc
|
||||
if code == "session_exists":
|
||||
raise HTTPException(status_code=409, detail="Session already exists") from exc
|
||||
raise HTTPException(status_code=400, detail=code) from exc
|
||||
return {"session_id": body.session_id, "viewport_w": body.viewport_w, "viewport_h": body.viewport_h}
|
||||
|
||||
|
||||
@app.delete("/v1/sessions/{session_id}")
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
x_nexus_worker_key: str | None = Header(default=None, alias="X-Nexus-Worker-Key"),
|
||||
):
|
||||
_check_key(x_nexus_worker_key)
|
||||
if not await manager.destroy_session(session_id):
|
||||
raise HTTPException(status_code=404, detail="Session not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.websocket("/v1/sessions/{session_id}/stream")
|
||||
async def session_stream(
|
||||
websocket: WebSocket,
|
||||
session_id: str,
|
||||
x_nexus_worker_key: str | None = Header(default=None, alias="X-Nexus-Worker-Key"),
|
||||
):
|
||||
key = x_nexus_worker_key or websocket.headers.get("x-nexus-worker-key")
|
||||
if not settings.WORKER_SECRET or key != settings.WORKER_SECRET:
|
||||
await websocket.close(code=4401, reason="Invalid worker key")
|
||||
return
|
||||
|
||||
if not manager.get_session(session_id):
|
||||
await websocket.close(code=4404, reason="Session not found")
|
||||
return
|
||||
|
||||
await websocket.accept()
|
||||
|
||||
outbound: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
|
||||
async def send(msg: dict[str, Any]) -> None:
|
||||
await outbound.put(msg)
|
||||
|
||||
try:
|
||||
await manager.attach_stream(session_id, send)
|
||||
except ValueError:
|
||||
await websocket.close(code=4404, reason="Session not found")
|
||||
return
|
||||
except RuntimeError:
|
||||
await manager.destroy_session(session_id)
|
||||
await websocket.close(code=4503, reason="Browser not ready")
|
||||
return
|
||||
|
||||
async def pump_out() -> None:
|
||||
while True:
|
||||
msg = await outbound.get()
|
||||
await websocket.send_json(msg)
|
||||
|
||||
pump_task = asyncio.create_task(pump_out(), name=f"ws_out_{session_id}")
|
||||
try:
|
||||
while True:
|
||||
raw = await websocket.receive_text()
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
await send({"type": "ERROR", "message": "Invalid JSON", "code": "BAD_JSON"})
|
||||
continue
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
await manager.handle_message(session_id, msg)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
pump_task.cancel()
|
||||
try:
|
||||
await pump_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
await manager.destroy_session(session_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
host, _, port = settings.WORKER_BIND.partition(":")
|
||||
uvicorn.run("main:app", host=host or "0.0.0.0", port=int(port or 8443), log_level="info")
|
||||
@@ -1,4 +0,0 @@
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
playwright==1.49.1
|
||||
pydantic-settings==2.7.1
|
||||
@@ -1,346 +0,0 @@
|
||||
"""Playwright Chromium session manager with CDP screencast."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Awaitable
|
||||
|
||||
from playwright.async_api import Browser, BrowserContext, Page, async_playwright
|
||||
|
||||
from browser_stealth import (
|
||||
CHROMIUM_ARGS,
|
||||
CONTEXT_OPTIONS,
|
||||
IGNORE_DEFAULT_ARGS,
|
||||
STEALTH_INIT_SCRIPT,
|
||||
)
|
||||
from browser_url_safe import validate_navigation_url
|
||||
from config import settings
|
||||
|
||||
|
||||
def _headless() -> bool:
|
||||
return settings.WORKER_HEADLESS.lower() in ("1", "true", "yes")
|
||||
|
||||
logger = logging.getLogger("browser_worker.session")
|
||||
|
||||
SendFn = Callable[[dict[str, Any]], Awaitable[None]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BrowserSession:
|
||||
session_id: str
|
||||
viewport_w: int
|
||||
viewport_h: int
|
||||
send: SendFn
|
||||
ready: bool = False
|
||||
context: BrowserContext | None = None
|
||||
page: Page | None = None
|
||||
cdp: Any = None
|
||||
last_activity: float = field(default_factory=time.monotonic)
|
||||
reserved_at: float = field(default_factory=time.monotonic)
|
||||
_screencast_task: asyncio.Task | None = None
|
||||
_title_task: asyncio.Task | None = None
|
||||
|
||||
async def touch(self) -> None:
|
||||
self.last_activity = time.monotonic()
|
||||
|
||||
async def close(self) -> None:
|
||||
for task in (self._screencast_task, self._title_task):
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._screencast_task = None
|
||||
self._title_task = None
|
||||
if self.cdp:
|
||||
try:
|
||||
await self.cdp.send("Page.stopScreencast")
|
||||
except Exception:
|
||||
pass
|
||||
self.cdp = None
|
||||
if self.context:
|
||||
try:
|
||||
await self.context.close()
|
||||
except Exception as exc:
|
||||
logger.warning("context close failed session=%s: %s", self.session_id, exc)
|
||||
self.context = None
|
||||
self.page = None
|
||||
self.ready = False
|
||||
|
||||
|
||||
class SessionManager:
|
||||
def __init__(self) -> None:
|
||||
self._browser: Browser | None = None
|
||||
self._playwright = None
|
||||
self._sessions: dict[str, BrowserSession] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._idle_task: asyncio.Task | None = None
|
||||
|
||||
async def start(self) -> None:
|
||||
self._playwright = await async_playwright().start()
|
||||
self._browser = await self._playwright.chromium.launch(
|
||||
headless=_headless(),
|
||||
args=CHROMIUM_ARGS,
|
||||
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||
)
|
||||
self._idle_task = asyncio.create_task(self._idle_loop(), name="browser_idle")
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._idle_task:
|
||||
self._idle_task.cancel()
|
||||
try:
|
||||
await self._idle_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
for sid in list(self._sessions):
|
||||
await self.destroy_session(sid)
|
||||
if self._browser:
|
||||
await self._browser.close()
|
||||
self._browser = None
|
||||
if self._playwright:
|
||||
await self._playwright.stop()
|
||||
self._playwright = None
|
||||
|
||||
async def _idle_loop(self) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
now = time.monotonic()
|
||||
stale: list[str] = []
|
||||
for sid, sess in self._sessions.items():
|
||||
if sess.ready:
|
||||
if now - sess.last_activity > settings.WORKER_IDLE_TIMEOUT_SEC:
|
||||
stale.append(sid)
|
||||
elif now - sess.reserved_at > settings.WORKER_RESERVE_TIMEOUT_SEC:
|
||||
stale.append(sid)
|
||||
for sid in stale:
|
||||
logger.info("idle timeout session=%s", sid)
|
||||
await self.destroy_session(sid)
|
||||
|
||||
def session_count(self) -> int:
|
||||
return len(self._sessions)
|
||||
|
||||
async def reserve_session(
|
||||
self,
|
||||
session_id: str,
|
||||
viewport_w: int,
|
||||
viewport_h: int,
|
||||
) -> None:
|
||||
async with self._lock:
|
||||
if session_id in self._sessions:
|
||||
raise ValueError("session_exists")
|
||||
if len(self._sessions) >= settings.WORKER_MAX_SESSIONS:
|
||||
raise ValueError("max_sessions")
|
||||
self._sessions[session_id] = BrowserSession(
|
||||
session_id=session_id,
|
||||
viewport_w=viewport_w,
|
||||
viewport_h=viewport_h,
|
||||
send=self._noop_send,
|
||||
)
|
||||
|
||||
async def attach_stream(self, session_id: str, send: SendFn) -> BrowserSession:
|
||||
async with self._lock:
|
||||
session = self._sessions.get(session_id)
|
||||
if not session:
|
||||
raise ValueError("session_not_found")
|
||||
if session.ready:
|
||||
session.send = send
|
||||
return session
|
||||
if not self._browser:
|
||||
raise RuntimeError("browser_not_ready")
|
||||
session.send = send
|
||||
|
||||
context = await self._browser.new_context(
|
||||
viewport={"width": session.viewport_w, "height": session.viewport_h},
|
||||
ignore_https_errors=False,
|
||||
**CONTEXT_OPTIONS,
|
||||
)
|
||||
await context.add_init_script(STEALTH_INIT_SCRIPT)
|
||||
page = await context.new_page()
|
||||
session.context = context
|
||||
session.page = page
|
||||
cdp = await context.new_cdp_session(page)
|
||||
session.cdp = cdp
|
||||
await cdp.send("Page.startScreencast", {
|
||||
"format": "jpeg",
|
||||
"quality": max(60, min(settings.WORKER_SCREENCAST_QUALITY, 100)),
|
||||
"everyNthFrame": 1,
|
||||
})
|
||||
session._screencast_task = asyncio.create_task(
|
||||
self._screencast_loop(session, cdp),
|
||||
name=f"screencast_{session_id}",
|
||||
)
|
||||
session._title_task = asyncio.create_task(
|
||||
self._title_loop(session),
|
||||
name=f"title_{session_id}",
|
||||
)
|
||||
session.ready = True
|
||||
await session.send({
|
||||
"type": "BROWSER_INIT",
|
||||
"session_id": session_id,
|
||||
"viewport_w": session.viewport_w,
|
||||
"viewport_h": session.viewport_h,
|
||||
})
|
||||
return session
|
||||
|
||||
async def _noop_send(self, _msg: dict[str, Any]) -> None:
|
||||
return
|
||||
|
||||
async def destroy_session(self, session_id: str) -> bool:
|
||||
async with self._lock:
|
||||
session = self._sessions.pop(session_id, None)
|
||||
if not session:
|
||||
return False
|
||||
await session.close()
|
||||
return True
|
||||
|
||||
def get_session(self, session_id: str) -> BrowserSession | None:
|
||||
return self._sessions.get(session_id)
|
||||
|
||||
async def _screencast_loop(self, session: BrowserSession, cdp: Any) -> None:
|
||||
"""CDPSession has no wait_for_event — use on() + asyncio.Queue."""
|
||||
frame_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
|
||||
def _on_screencast_frame(params: dict[str, Any]) -> None:
|
||||
try:
|
||||
frame_queue.put_nowait(params)
|
||||
except asyncio.QueueFull:
|
||||
pass
|
||||
|
||||
cdp.on("Page.screencastFrame", _on_screencast_frame)
|
||||
try:
|
||||
while session.session_id in self._sessions and session.ready:
|
||||
try:
|
||||
msg = await asyncio.wait_for(frame_queue.get(), timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
await session.touch()
|
||||
await session.send({
|
||||
"type": "SCREENCAST_FRAME",
|
||||
"data_b64": msg.get("data", ""),
|
||||
"metadata": msg.get("metadata") or {},
|
||||
})
|
||||
await cdp.send("Page.screencastFrameAck", {"sessionId": msg["sessionId"]})
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning("screencast ended session=%s: %s", session.session_id, exc)
|
||||
|
||||
async def _title_loop(self, session: BrowserSession) -> None:
|
||||
page = session.page
|
||||
if not page:
|
||||
return
|
||||
last_url = ""
|
||||
last_title = ""
|
||||
try:
|
||||
while session.session_id in self._sessions and session.ready:
|
||||
await asyncio.sleep(1)
|
||||
await session.touch()
|
||||
try:
|
||||
url = page.url
|
||||
title = await page.title()
|
||||
except Exception:
|
||||
continue
|
||||
if url != last_url or title != last_title:
|
||||
last_url = url
|
||||
last_title = title
|
||||
await session.send({
|
||||
"type": "PAGE_INFO",
|
||||
"url": url,
|
||||
"title": title,
|
||||
})
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
async def handle_message(self, session_id: str, msg: dict[str, Any]) -> None:
|
||||
session = self.get_session(session_id)
|
||||
if not session or not session.page or not session.ready:
|
||||
return
|
||||
await session.touch()
|
||||
page = session.page
|
||||
mtype = msg.get("type")
|
||||
|
||||
if mtype == "PING":
|
||||
await session.send({"type": "PONG"})
|
||||
return
|
||||
|
||||
if mtype == "NAVIGATE":
|
||||
url = msg.get("url", "")
|
||||
normalized, err = validate_navigation_url(str(url))
|
||||
if err:
|
||||
await session.send({"type": "ERROR", "message": err, "code": "URL_BLOCKED"})
|
||||
return
|
||||
try:
|
||||
await page.goto(normalized, wait_until="domcontentloaded", timeout=30_000)
|
||||
await session.send({
|
||||
"type": "PAGE_INFO",
|
||||
"url": page.url,
|
||||
"title": await page.title(),
|
||||
})
|
||||
except Exception as exc:
|
||||
await session.send({
|
||||
"type": "ERROR",
|
||||
"message": str(exc)[:500],
|
||||
"code": "NAVIGATE_FAILED",
|
||||
})
|
||||
return
|
||||
|
||||
if mtype == "RELOAD":
|
||||
await page.reload(wait_until="domcontentloaded", timeout=30_000)
|
||||
return
|
||||
if mtype == "GO_BACK":
|
||||
await page.go_back(wait_until="domcontentloaded", timeout=30_000)
|
||||
return
|
||||
if mtype == "GO_FORWARD":
|
||||
await page.go_forward(wait_until="domcontentloaded", timeout=30_000)
|
||||
return
|
||||
|
||||
if mtype == "VIEWPORT":
|
||||
w = int(msg.get("width") or session.viewport_w)
|
||||
h = int(msg.get("height") or session.viewport_h)
|
||||
w = max(320, min(w, 1920))
|
||||
h = max(240, min(h, 1080))
|
||||
session.viewport_w = w
|
||||
session.viewport_h = h
|
||||
await page.set_viewport_size({"width": w, "height": h})
|
||||
return
|
||||
|
||||
if mtype == "MOUSE":
|
||||
event = msg.get("event")
|
||||
x = float(msg.get("x", 0))
|
||||
y = float(msg.get("y", 0))
|
||||
button = msg.get("button", "left")
|
||||
steps = int(msg.get("steps") or 1)
|
||||
steps = max(1, min(steps, 25))
|
||||
if event == "move":
|
||||
await page.mouse.move(x, y, steps=steps)
|
||||
elif event == "down":
|
||||
await page.mouse.move(x, y)
|
||||
await page.mouse.down(button=button)
|
||||
elif event == "up":
|
||||
await page.mouse.move(x, y)
|
||||
await page.mouse.up(button=button)
|
||||
elif event == "click":
|
||||
await page.mouse.click(x, y, button=button, delay=50)
|
||||
elif event == "wheel":
|
||||
delta_y = float(msg.get("delta_y", 0))
|
||||
await page.mouse.wheel(0, delta_y)
|
||||
return
|
||||
|
||||
if mtype == "KEY":
|
||||
event = msg.get("event")
|
||||
key = msg.get("key", "")
|
||||
text = msg.get("text", "")
|
||||
if event == "down" and key:
|
||||
await page.keyboard.down(key)
|
||||
elif event == "up" and key:
|
||||
await page.keyboard.up(key)
|
||||
elif event == "type" and text:
|
||||
await page.keyboard.type(text)
|
||||
return
|
||||
|
||||
|
||||
manager = SessionManager()
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -2087,3 +2087,898 @@
|
||||
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T01:45:17+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-telegram-chat-id-group-detect.md 29lines"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-offline-quick-cmd-telegram.md"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T02:05:03+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-telegram-chat-id-group-detect.md 29lines"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-telegram-chat-id-group-detect.md"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T02:05:27+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-telegram-chat-id-group-detect.md 29lines"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-telegram-chat-id-group-detect.md"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T09:08:25+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-files-new-file-fix.md 30lines"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-files-new-file-fix.md"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T09:09:16+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-files-new-file-fix.md 30lines"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-files-new-file-fix.md"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T09:49:22+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-slot-hardware-specs.md 34lines"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-slot-hardware-specs.md"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T14:12:12+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-slot-hardware-specs.md 34lines"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-slot-hardware-specs.md"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T14:12:37+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-ssh-watch-probe-quote-fix.md 22lines"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-ssh-watch-probe-quote-fix.md"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:10:32+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-ssh-watch-probe-quote-fix.md 22lines"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-ssh-watch-probe-quote-fix.md"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:10:53+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-ssh-watch-probe-quote-fix.md"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:23:12+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-beijing-remove-browser.md"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:30:50+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-beijing-remove-browser.md"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:31:16+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-beijing-remove-browser.md"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:31:34+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-beijing-remove-browser.md"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:31:45+08:00","date":"2026-06-13","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-history-beijing-time.md 27lines"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-beijing-remove-browser.md"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T16:32:02+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"changelog","result":"PASS","detail":"2026-06-13-watch-ui-polish.md 30lines"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"audit","result":"PASS","detail":"2026-06-13-watch-ui-polish.md"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-13T23:30:02+08:00","date":"2026-06-13","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"changelog","result":"BLOCK","detail":"file not found"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"audit","result":"BLOCK","detail":"file not found"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"review","result":"BLOCK","detail":"no audit file"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T00:38:17+08:00","date":"2026-06-14","gate":"summary","result":"BLOCK","detail":"4/7"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"changelog","result":"BLOCK","detail":"file not found"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"audit","result":"BLOCK","detail":"file not found"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"review","result":"BLOCK","detail":"no audit file"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T00:38:28+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-slot-paused-center-switch.md 23lines"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-slot-paused-center-switch.md"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T00:38:44+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-eager-probe-on-resume.md 39lines"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-eager-probe-on-resume.md"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T02:53:05+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-metric-ring-baota-style.md 20lines"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-ssh-ring-baota.md"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T03:19:48+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-metric-ring-baota-style.md 20lines"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-ssh-ring-baota.md"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T03:25:10+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-slot-paused-ui-polish.md 27lines"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-slot-paused-ui-polish.md"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T03:34:17+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-slot-paused-ui-polish.md 27lines"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-slot-paused-ui-polish.md"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T04:16:26+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-layer3-health-monitor-p2-p3-ui.md 34lines"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-layer3-health-monitor-p2-p3-ui.md"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T04:20:09+08:00","date":"2026-06-14","gate":"summary","result":"BLOCK","detail":"5/7"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-layer3-health-monitor-p2-p3-ui.md 35lines"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-layer3-health-monitor-p2-p3-ui.md"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T04:20:43+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-watch-baota-server-detail.md 25lines"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-watch-baota-server-detail.md"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T06:23:45+08:00","date":"2026-06-14","gate":"summary","result":"PASS","detail":"7/7"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"changelog","result":"PASS","detail":"2026-06-14-gate-ai-review.md 42lines"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"audit","result":"PASS","detail":"2026-06-14-gate-ai-review.md"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"ai_review","result":"PASS","detail":"skipped no code"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||
{"ts":"2026-06-14T19:03:03+08:00","date":"2026-06-14","gate":"summary","result":"BLOCK","detail":"6/8"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"changelog","result":"PASS","detail":"2026-06-17-terminal-ui-deploy.md 34lines"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"audit","result":"PASS","detail":"2026-06-17-terminal-ui-deploy.md"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"test","result":"PASS","detail":"all passed"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"import","result":"PASS","detail":"ok"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"review","result":"BLOCK","detail":"audit missing changed files"}
|
||||
{"ts":"2026-06-17T09:37:14+08:00","date":"2026-06-17","gate":"ai_review","result":"BLOCK","detail":"see gate_ai_review.py output"}
|
||||
{"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"}
|
||||
|
||||
+145
-16
@@ -9,12 +9,64 @@ set -euo pipefail
|
||||
|
||||
DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-/opt/nexus}"
|
||||
NEXUS_SERVICE="${NEXUS_SERVICE:-nexus}"
|
||||
FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||
STATE_DIR=""
|
||||
FAIL_FILE=""
|
||||
LAST_RESTART_FILE=""
|
||||
# Legacy paths (cron user may have created root-owned files under /tmp)
|
||||
LEGACY_FAIL_FILE="/tmp/nexus_health_fail_count"
|
||||
LEGACY_LAST_RESTART_FILE="/tmp/nexus_health_last_restart"
|
||||
MAX_FAIL=3
|
||||
RESTART_COOLDOWN_SEC="${NEXUS_HEALTH_RESTART_COOLDOWN:-300}"
|
||||
ENV_PROD="${DEPLOY_DIR}/docker/.env.prod"
|
||||
|
||||
ensure_state_paths() {
|
||||
if [[ -n "$STATE_DIR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local preferred="${DEPLOY_DIR}/var/layer3-health"
|
||||
local fallback="${HOME:-/tmp}/.nexus/layer3-health"
|
||||
if mkdir -p "$preferred" 2>/dev/null && [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo mkdir -p "$preferred" 2>/dev/null \
|
||||
&& sudo chown -R "$(id -un):$(id -gn)" "$preferred" 2>/dev/null \
|
||||
&& [[ -w "$preferred" ]]; then
|
||||
STATE_DIR="$preferred"
|
||||
else
|
||||
mkdir -p "$fallback"
|
||||
STATE_DIR="$fallback"
|
||||
fi
|
||||
FAIL_FILE="${STATE_DIR}/fail_count"
|
||||
LAST_RESTART_FILE="${STATE_DIR}/last_restart"
|
||||
}
|
||||
|
||||
docker_cmd() {
|
||||
if docker "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo docker "$@" 2>/dev/null
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
init_layer3_state() {
|
||||
ensure_state_paths
|
||||
chmod 755 "$STATE_DIR" 2>/dev/null || true
|
||||
if [[ ! -f "$FAIL_FILE" && -f "$LEGACY_FAIL_FILE" ]]; then
|
||||
cp "$LEGACY_FAIL_FILE" "$FAIL_FILE" 2>/dev/null || true
|
||||
fi
|
||||
if [[ ! -f "$LAST_RESTART_FILE" && -f "$LEGACY_LAST_RESTART_FILE" ]]; then
|
||||
cp "$LEGACY_LAST_RESTART_FILE" "$LAST_RESTART_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_file() {
|
||||
local path="$1" value="$2"
|
||||
init_layer3_state
|
||||
printf '%s\n' "$value" >"$path" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
resolve_publish_port() {
|
||||
local port
|
||||
port="$(grep -E '^NEXUS_PUBLISH_PORT=' "$ENV_PROD" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r" ' || true)"
|
||||
@@ -25,7 +77,7 @@ PUBLISH_PORT="$(resolve_publish_port)"
|
||||
HEALTH_URL="http://127.0.0.1:${PUBLISH_PORT}/health"
|
||||
|
||||
resolve_nexus_container() {
|
||||
docker ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
docker_cmd ps --format '{{.Names}}' 2>/dev/null | grep -E 'nexus-prod-nexus|nexus-nexus-1' | head -1 || true
|
||||
}
|
||||
|
||||
read_env_var() {
|
||||
@@ -50,7 +102,7 @@ load_telegram_from_db() {
|
||||
[[ -n "$cname" ]] || return 1
|
||||
[[ -n "${BOT_TOKEN:-}" && -n "${CHAT_ID:-}" ]] && return 0
|
||||
local out token chat
|
||||
out="$(docker exec "$cname" python3 -c "
|
||||
out="$(docker_cmd exec "$cname" python3 -c "
|
||||
import asyncio
|
||||
from server.config import settings as s
|
||||
from server.infrastructure.database.session import AsyncSessionLocal
|
||||
@@ -94,7 +146,7 @@ load_telegram_config() {
|
||||
NEXUS_TELEGRAM_BOT_TOKEN=*) BOT_TOKEN="${line#NEXUS_TELEGRAM_BOT_TOKEN=}" ;;
|
||||
NEXUS_TELEGRAM_CHAT_ID=*) CHAT_ID="${line#NEXUS_TELEGRAM_CHAT_ID=}" ;;
|
||||
esac
|
||||
done < <(docker exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
done < <(docker_cmd exec "$cname" grep -E '^NEXUS_TELEGRAM_' /app/.env 2>/dev/null || true)
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] && return 0
|
||||
load_telegram_from_db "$cname" || true
|
||||
fi
|
||||
@@ -103,7 +155,7 @@ load_telegram_config() {
|
||||
}
|
||||
|
||||
send_telegram() {
|
||||
local msg="${1//\\n/$'\n'}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
local msg="${1}" resp http log="${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}"
|
||||
BOT_TOKEN="$(normalize_env_value "${BOT_TOKEN:-}")"
|
||||
CHAT_ID="$(normalize_env_value "${CHAT_ID:-}")"
|
||||
[[ -n "$BOT_TOKEN" && -n "$CHAT_ID" ]] || return 1
|
||||
@@ -121,24 +173,25 @@ send_telegram() {
|
||||
|
||||
bump_fail_count() {
|
||||
local fail=0
|
||||
init_layer3_state
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
flock -x 9
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
echo "$fail" > "$FAIL_FILE"
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
) 9>"${FAIL_FILE}.lock"
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
else
|
||||
fail="$(cat "$FAIL_FILE" 2>/dev/null || echo 0)"
|
||||
fail=$((fail + 1))
|
||||
echo "$fail" > "$FAIL_FILE"
|
||||
write_state_file "$FAIL_FILE" "$fail" || true
|
||||
fi
|
||||
echo "$fail"
|
||||
}
|
||||
|
||||
reset_fail_count() {
|
||||
echo "0" > "$FAIL_FILE"
|
||||
write_state_file "$FAIL_FILE" "0" || true
|
||||
}
|
||||
|
||||
restart_cooldown_active() {
|
||||
@@ -151,13 +204,13 @@ restart_cooldown_active() {
|
||||
}
|
||||
|
||||
mark_restart_attempt() {
|
||||
date +%s > "$LAST_RESTART_FILE"
|
||||
write_state_file "$LAST_RESTART_FILE" "$(date +%s)" || true
|
||||
}
|
||||
|
||||
restart_nexus() {
|
||||
local cname="$1" via=""
|
||||
if [[ -n "$cname" ]]; then
|
||||
docker restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||
docker_cmd restart "$cname" >/dev/null 2>&1 && via="Docker 容器 ${cname}"
|
||||
elif command -v supervisorctl >/dev/null 2>&1 && supervisorctl status "$NEXUS_SERVICE" >/dev/null 2>&1; then
|
||||
supervisorctl restart "$NEXUS_SERVICE" >/dev/null 2>&1 && via="Supervisor ${NEXUS_SERVICE}"
|
||||
else
|
||||
@@ -166,8 +219,84 @@ restart_nexus() {
|
||||
echo "$via"
|
||||
}
|
||||
|
||||
telegram_health_fail_cooldown() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:已达 ${MAX_FAIL} 次
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动
|
||||
建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 ${NEXUS_HEALTH_LOG:-/var/log/nexus_health.log}
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_fail_restart() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)
|
||||
Docker 容器:${NEXUS_CONTAINER:-未识别}
|
||||
具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常
|
||||
正在执行:自动重启 Nexus 服务…
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_recovered() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
恢复方式:${via}
|
||||
具体情况:自动重启后 /health 检测通过,服务已恢复在线
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_still_down() {
|
||||
local via="$1" body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
已尝试:${via}
|
||||
具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误
|
||||
建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
telegram_health_restart_failed() {
|
||||
local body
|
||||
body="$(cat <<EOF
|
||||
🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>
|
||||
宿主机:${HOST_LABEL}
|
||||
检测地址:${HEALTH_URL}
|
||||
具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})
|
||||
建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update
|
||||
时间:${TS_LABEL}
|
||||
EOF
|
||||
)"
|
||||
send_telegram "$body" || true
|
||||
}
|
||||
|
||||
NEXUS_CONTAINER="$(resolve_nexus_container)"
|
||||
load_telegram_config "$NEXUS_CONTAINER"
|
||||
init_layer3_state
|
||||
HOST_LABEL="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
||||
TS_LABEL="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || date)"
|
||||
|
||||
@@ -182,21 +311,21 @@ if [[ "$FAIL" -lt "$MAX_FAIL" ]]; then
|
||||
fi
|
||||
|
||||
if restart_cooldown_active; then
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】健康检查连续失败(冷却期内)</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n连续失败:已达 ${MAX_FAIL} 次\nDocker 容器:${NEXUS_CONTAINER:-未识别}\n具体情况:/health 仍不可达,但距上次自动重启不足 ${RESTART_COOLDOWN_SEC} 秒,本次跳过重启以免频繁抖动\n建议操作:查看 docker logs ${NEXUS_CONTAINER:-nexus} 与 /var/log/nexus_health.log\n时间:${TS_LABEL}" || true
|
||||
telegram_health_fail_cooldown
|
||||
exit 0
|
||||
fi
|
||||
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】Nexus 连续 ${MAX_FAIL} 次无响应</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n连续失败:${FAIL} 次(阈值 ${MAX_FAIL} 次)\nDocker 容器:${NEXUS_CONTAINER:-未识别}\n具体情况:宿主机 cron 无法访问 API 健康端点,平台可能已宕机或端口异常\n正在执行:自动重启 Nexus 服务…\n时间:${TS_LABEL}" || true
|
||||
telegram_health_fail_restart
|
||||
mark_restart_attempt
|
||||
|
||||
if VIA="$(restart_nexus "$NEXUS_CONTAINER")"; then
|
||||
sleep 5
|
||||
if curl -sf --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then
|
||||
send_telegram "🟢 <b>【Layer 3 宿主机巡检】Nexus 已自动恢复</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n恢复方式:${VIA}\n具体情况:自动重启后 /health 检测通过,服务已恢复在线\n时间:${TS_LABEL}" || true
|
||||
telegram_health_recovered "$VIA"
|
||||
reset_fail_count
|
||||
else
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】自动重启后仍未恢复</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n已尝试:${VIA}\n具体情况:重启完成后 /health 仍失败,可能存在数据库/Redis 故障或配置错误\n建议操作:立即 SSH 登录排查 docker logs、MySQL、Redis,需人工介入\n时间:${TS_LABEL}" || true
|
||||
telegram_health_still_down "$VIA"
|
||||
fi
|
||||
else
|
||||
send_telegram "🔴 <b>【Layer 3 宿主机巡检】无法执行自动重启</b>\n宿主机:${HOST_LABEL}\n检测地址:${HEALTH_URL}\n具体情况:未找到可重启的 Docker 容器或 Supervisor 服务(${NEXUS_SERVICE})\n建议操作:确认 nexus 容器名称与 docker ps 输出,手动执行 docker restart 或 nx update\n时间:${TS_LABEL}" || true
|
||||
telegram_health_restart_failed
|
||||
fi
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
@@ -1,21 +0,0 @@
|
||||
# 复制: cp deploy/nexus-1panel.secrets.sh.example deploy/nexus-1panel.secrets.sh
|
||||
# 用于 git push(scripts/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'
|
||||
|
||||
# 本机生产 SSH(deploy-production.sh 读取;私钥路径勿提交真实 .pem)
|
||||
# NEXUS_SSH='azureuser@20.24.218.235'
|
||||
# NEXUS_SSH_KEY='/path/to/nz-admin.pem'
|
||||
Executable → Regular
+26
-5
@@ -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 ;;
|
||||
|
||||
+64
-16
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Nexus Pre-deploy Gate Check (v2 — 7 gates)
|
||||
# 在部署前强制检查 7 道门控,任何一道不过则阻止部署
|
||||
# Nexus Pre-deploy Gate Check (v3 — 8 gates)
|
||||
# 在部署前强制检查 8 道门控,任何一道不过则阻止部署
|
||||
#
|
||||
# 门控1: CHANGELOG — docs/changelog/ 下必须有今天的 .md 文件(且行数≥10)
|
||||
# 门控2: AUDIT — docs/audit/ 下必须有今天的审计记录(且包含关键段落)
|
||||
@@ -9,6 +9,7 @@
|
||||
# 门控5: IMPORT — python -c "import server.main" 成功
|
||||
# 门控6: SECURITY — bandit -r server/ 无 HIGH/MEDIUM
|
||||
# 门控7: REVIEW — 审计文件包含 Closure表+文件清单+DoD,且文件清单与 git diff 交叉验证
|
||||
# 门控8: AI_REVIEW — cursor-agent 审查 HEAD~1..HEAD;缓存 docs/reviews/<head_sha>.json
|
||||
#
|
||||
# 用法: bash deploy/pre_deploy_check.sh
|
||||
# 退出码: 0=全部通过, 1=至少一道门未过
|
||||
@@ -22,7 +23,7 @@ DEPLOY_DIR="${NEXUS_DEPLOY_DIR:-${_REPO_ROOT}}"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TIMESTAMP=$(date -Iseconds)
|
||||
GATES_PASSED=0
|
||||
GATES_TOTAL=7
|
||||
GATES_TOTAL=8
|
||||
BLOCKED=0
|
||||
GATE_RESULTS=""
|
||||
|
||||
@@ -53,14 +54,14 @@ gate_log() {
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " Nexus Pre-deploy Gate Check (v2)"
|
||||
echo " Nexus Pre-deploy Gate Check (v3)"
|
||||
echo " Date: ${TODAY}"
|
||||
echo " Gates: ${GATES_TOTAL}"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# ── Gate 1: Changelog (存在 + 行数≥10) ──
|
||||
echo -n "Gate 1/7: Changelog ... "
|
||||
echo -n "Gate 1/8: Changelog ... "
|
||||
CHANGELOG_DIR="${DEPLOY_DIR}/docs/changelog"
|
||||
if ls "${CHANGELOG_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${CHANGELOG_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
@@ -86,7 +87,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 2: Audit (存在 + 关键段落) ──
|
||||
echo -n "Gate 2/7: Audit ... "
|
||||
echo -n "Gate 2/8: Audit ... "
|
||||
AUDIT_DIR="${DEPLOY_DIR}/docs/audit"
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
@@ -117,7 +118,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 3: Test (必须存在 + 必须通过) ──
|
||||
echo -n "Gate 3/7: Test ... "
|
||||
echo -n "Gate 3/8: Test ... "
|
||||
TEST_SCRIPT="${DEPLOY_DIR}/tests/test_api.py"
|
||||
if [ ! -f "${TEST_SCRIPT}" ]; then
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
@@ -130,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"
|
||||
@@ -145,7 +173,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 4: Lint (ruff — F only: undefined names, unused imports) ──
|
||||
echo -n "Gate 4/7: Lint ... "
|
||||
echo -n "Gate 4/8: Lint ... "
|
||||
RUFF_BIN=$(_pick_bin ruff || true)
|
||||
if [ -n "${RUFF_BIN}" ]; then
|
||||
LINT_OUTPUT=$(cd "${DEPLOY_DIR}" && "${RUFF_BIN}" check server/ --select F 2>&1) || true
|
||||
@@ -172,7 +200,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 5: Import (python -c "import server.main") ──
|
||||
echo -n "Gate 5/7: Import ... "
|
||||
echo -n "Gate 5/8: Import ... "
|
||||
IMPORT_PYTHON=""
|
||||
for p in "${DEPLOY_DIR}/.venv/bin/python3" "${DEPLOY_DIR}/venv/bin/python3"; do
|
||||
if [ -x "${p}" ]; then
|
||||
@@ -207,7 +235,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 6: Security (bandit — HIGH only) ──
|
||||
echo -n "Gate 6/7: Security ... "
|
||||
echo -n "Gate 6/8: Security ... "
|
||||
BANDIT_BIN=$(_pick_bin bandit || true)
|
||||
if [ -n "${BANDIT_BIN}" ]; then
|
||||
SEC_OUTPUT=$(cd "${DEPLOY_DIR}" && "${BANDIT_BIN}" -r server/ -f txt -ll 2>&1) || true
|
||||
@@ -234,7 +262,7 @@ else
|
||||
fi
|
||||
|
||||
# ── Gate 7: Review (审计文件交叉验证) ──
|
||||
echo -n "Gate 7/7: Review ... "
|
||||
echo -n "Gate 7/8: Review ... "
|
||||
if ls "${AUDIT_DIR}/${TODAY}-"*.md 1>/dev/null 2>&1; then
|
||||
FILE=$(ls -t "${AUDIT_DIR}/${TODAY}-"*.md 2>/dev/null | head -1)
|
||||
# 检查审计文件中是否列出了实际改动的文件
|
||||
@@ -273,6 +301,26 @@ else
|
||||
gate_log "review" "BLOCK" "no audit file"
|
||||
fi
|
||||
|
||||
# ── Gate 8: AI Review (cursor-agent + docs/reviews cache) ──
|
||||
echo -n "Gate 8/8: AI Review ... "
|
||||
AI_REVIEW_OUT=$(cd "${DEPLOY_DIR}" && python3 "${DEPLOY_DIR}/scripts/gate_ai_review.py" 2>&1) || AI_REVIEW_RC=$?
|
||||
AI_REVIEW_RC=${AI_REVIEW_RC:-0}
|
||||
if [ "${AI_REVIEW_RC}" -eq 0 ]; then
|
||||
echo -e "${GREEN}PASS${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ └─ /' | head -5
|
||||
GATES_PASSED=$((GATES_PASSED + 1))
|
||||
if echo "${AI_REVIEW_OUT}" | grep -qi "skip"; then
|
||||
gate_log "ai_review" "PASS" "skipped no code"
|
||||
else
|
||||
gate_log "ai_review" "PASS" "ok"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}BLOCK${NC}"
|
||||
echo "${AI_REVIEW_OUT}" | sed 's/^/ │ /' | head -12
|
||||
BLOCKED=1
|
||||
gate_log "ai_review" "BLOCK" "see gate_ai_review.py output"
|
||||
fi
|
||||
|
||||
# ── Pre-flight: shell scripts LF ──
|
||||
echo -n "Pre-flight: Shell EOL (LF) ... "
|
||||
if bash "${DEPLOY_DIR}/deploy/check_shell_eol.sh" >/dev/null 2>&1; then
|
||||
|
||||
@@ -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
@@ -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})"
|
||||
Executable → Regular
@@ -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 "$@"
|
||||
|
||||
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
+15
-8
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
# 1Panel:nx 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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 Token(30 分钟)和 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 存于 localStorage,XSS 漏洞可能导致 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>系统时间偏差 >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 连通,与真实推送消耗相同时间(通常 <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_monitor(30s)</td><td>Redis/MySQL 不可达</td><td>发 Telegram 告警/恢复</td></tr>
|
||||
<tr><td>Layer 3</td><td>Shell cron(1 分钟)</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>验收 L0~L5</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>首次部署 E2E(T1~T5、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,35 @@
|
||||
# 审计 — 文件管理「新建文件」修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-files-new-file-fix.md`
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `frontend/src/composables/files/useFilesActions.ts` | `openNewFileDialog`、等待编辑器就绪 |
|
||||
| `frontend/src/composables/files/useFilesEditor.ts` | 导出 `waitForEditorWorkbench` |
|
||||
| `frontend/src/composables/files/useFilesPage.ts` | 透传 helper |
|
||||
| `frontend/src/components/files/FilesToolbar.vue` | 未选服务器时提示 |
|
||||
| `server/infrastructure/ssh/asyncssh_pool.py` | SFTP 写前确保父目录 |
|
||||
|
||||
## Step 3(规则扫描)
|
||||
|
||||
| 规则 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| 鉴权 | PASS | 仍走 `/sync/write-file` + admin JWT |
|
||||
| 路径 | PASS | `validatePathSegment` 不变 |
|
||||
| 静默失败 | PASS | 编辑器未就绪有 warning snackbar |
|
||||
| SSH | PASS | `mkdir -p` 仅父目录 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 文件 | 结论 |
|
||||
|------|------|
|
||||
| `useFilesActions.ts` | SAFE — snackbar 错误提示 |
|
||||
| `asyncssh_pool.py` | SAFE — 复用 `_ensure_remote_parent_dir` |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog + 本审计
|
||||
- [x] frontend type-check
|
||||
- [ ] 生产验证新建文件
|
||||
@@ -0,0 +1,28 @@
|
||||
# 审计 — SSH 监测探针引号修复
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-ssh-watch-probe-quote-fix.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/infrastructure/ssh/remote_probe.py` | `strip(chr(34))` | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 探针脚本回归 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无新攻击面 | SAFE |
|
||||
| H2 | 恢复监测可用性 | SAFE — 热修复 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H2 | SAFE | 本地 bash 执行探针返回 status ok |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `test_ssh_watch_metrics_cmd_runs_locally` passed
|
||||
- [x] changelog / audit
|
||||
@@ -0,0 +1,69 @@
|
||||
# 审计 — 监测历史北京时间 + 下线远程浏览器
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-13-watch-history-beijing-time.md`
|
||||
- `docs/changelog/2026-06-12-remove-remote-browser.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `frontend/src/utils/datetime.ts` | `formatChartAxisBeijing` | ☑ |
|
||||
| `frontend/src/components/watch/WatchProbeRecordsTable.vue` | 北京时间列 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | tooltip 北京时间 | ☑ |
|
||||
| `frontend/src/components/watch/WatchTrendChart.vue` | 轴/tooltip 北京时间 | ☑ |
|
||||
| `frontend/src/pages/WatchMetricsPage.vue` | 「北京时间」标签 | ☑ |
|
||||
| `frontend/src/App.vue` | 移除 GlobalBrowserPanel | ☑ |
|
||||
| `frontend/src/router/index.ts` | 移除 `/browser` 路由 | ☑ |
|
||||
| `frontend/src/pages/ServersPage.vue` | 站点 `window.open` | ☑ |
|
||||
| `frontend/src/pages/AuditPage.vue` | 移除未用 import | ☑ |
|
||||
| `frontend/src/components/GlobalBrowserPanel.vue` | 删除 | ☑ |
|
||||
| `frontend/src/composables/useGlobalBrowser.ts` | 删除 | ☑ |
|
||||
| `frontend/src/composables/useRemoteBrowserSession.ts` | 删除 | ☑ |
|
||||
| `frontend/src/pages/BrowserPage.vue` | 删除 | ☑ |
|
||||
| `server/main.py` | 注销 browser 路由 | ☑ |
|
||||
| `server/api/browser.py` | 删除 | ☑ |
|
||||
| `server/api/browser_session.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/` | 删除 | ☑ |
|
||||
| `__init__.py` | browser 包删除 | ☑ |
|
||||
| `server/utils/browser_ui_state.py` | 删除 | ☑ |
|
||||
| `server/utils/browser_url_safe.py` | 删除 | ☑ |
|
||||
| `browser-worker/` | 删除 | ☑ |
|
||||
| `Dockerfile` | browser-worker 删除 | ☑ |
|
||||
| `browser-worker/main.py` | 删除 | ☑ |
|
||||
| `browser-worker/browser_url_safe.py` | 删除 | ☑ |
|
||||
| `browser-worker/browser_stealth.py` | 删除 | ☑ |
|
||||
| `browser-worker/config.py` | 删除 | ☑ |
|
||||
| `browser-worker/docker-compose.yml` | 删除 | ☑ |
|
||||
| `browser-worker/docker-entrypoint.sh` | 删除 | ☑ |
|
||||
| `browser-worker/requirements.txt` | 删除 | ☑ |
|
||||
| `browser-worker/session_manager.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/__init__.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/session_registry.py` | 删除 | ☑ |
|
||||
| `server/infrastructure/browser/worker_client.py` | 删除 | ☑ |
|
||||
| `tests/test_browser_ui_state.py` | 删除 | ☑ |
|
||||
| `tests/test_browser_url_safe.py` | 删除 | ☑ |
|
||||
| `tests/chain/test_agent_dashboard_flow.py` | Redis pipeline mock | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 北京时间仅前端展示,不改 API 时区 | SAFE |
|
||||
| H2 | 浏览器下线后无孤立 `/api/browser` 注册 | SAFE |
|
||||
| H3 | `browserUrl.ts` 保留供站点外链 | SAFE |
|
||||
| H4 | 无新密钥/明文 | SAFE |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | SAFE | L2b 26/26;chain 9/9;vite build OK |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `bash scripts/local_verify.sh`
|
||||
- [x] `pytest tests/chain -q`
|
||||
- [x] `cd frontend && npx vite build`
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产 `/health` + `#/watch-metrics` 北京时间终验
|
||||
@@ -0,0 +1,39 @@
|
||||
# 审计 — 监测插槽硬件详情
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-watch-slot-hardware-specs.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/utils/watch_metrics.py` | 硬件字段、fill_hardware_gaps | ☑ |
|
||||
| `server/infrastructure/ssh/remote_probe.py` | SSH 探针采集详情 | ☑ |
|
||||
| `server/background/watch_probe_runner.py` | 探针后补齐用量 | ☑ |
|
||||
| `web/agent/agent.py` | `_hardware_specs_dict` 心跳 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | 详情 UI | ☑ |
|
||||
| `frontend/src/components/watch/WatchMetricBar.vue` | sub 副行 | ☑ |
|
||||
| `frontend/src/utils/watchFormat.ts` | 格式化 | ☑ |
|
||||
| `frontend/src/composables/useWatchPins.ts` | 类型 | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 单测 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | cpu_model 长度截断 | SAFE — `_short_text` 120 字符 |
|
||||
| H2 | 无新公开端点 | SAFE — 复用 watch WS |
|
||||
| H3 | SSH 脚本无 shell 注入 | SAFE — 固定 python -c |
|
||||
| H4 | 旧 Agent 回退 | SAFE — `fill_hardware_gaps` 按 % 推算 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H4 | SAFE | `pytest tests/test_watch_metrics.py` 13/13 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] pytest watch_metrics passed
|
||||
- [x] changelog / audit
|
||||
- [x] 前端 build 由部署脚本执行
|
||||
- [x] 无密钥类配置变更
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 监测槽暂停态居中开关
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-watch-slot-paused-center-switch.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `WatchSlotCard.vue` | 暂停态居中开关、开启后才显示指标 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无新 API | SAFE — 仍 `emit('monitoring', …)` |
|
||||
| H2 | 误触历史 | SAFE — 暂停态卡片主体不跳转,仅「历史」按钮 |
|
||||
| H3 | 双开关冲突 | SAFE — 顶栏开关仅 `monitoringOn` 时显示;暂停仅中部开关开启 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H3 | SAFE | 纯前端 `WatchSlotCard.vue` 模板分支 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog
|
||||
- [x] `cd frontend && npx vite build`
|
||||
- [ ] 生产浏览器终验:暂停槽中部开关 → 开启后见 CPU/内存
|
||||
@@ -0,0 +1,70 @@
|
||||
# 审计 — 监测 UI 视觉迭代
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-13-watch-ui-polish.md`
|
||||
**范围**:监测历史页 / 趋势图 / 探针表 / 实时槽 — 纯前端视觉,无 API 或后端变更
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `frontend/src/pages/WatchMetricsPage.vue` | 卡片页头、共用时间窗、骨架屏/空态 | ☑ |
|
||||
| `frontend/src/components/watch/WatchTrendChart.vue` | 主题色 ECharts、暗色 tooltip/轴 | ☑ |
|
||||
| `frontend/src/components/watch/WatchProbeRecordsTable.vue` | 表容器、色阶、分页 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | I/O 网格、分隔线 | ☑ |
|
||||
| `frontend/src/components/watch/WatchMetricBar.vue` | 进度条高度/过渡 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | 主题色折线、渐变面积 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSlotRow.vue` | 行首标题区 | ☑ |
|
||||
| `frontend/src/utils/watchFormat.ts` | `metricPctClass` / `formatProbePct` | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | XSS / HTML 注入 | SAFE — 趋势 tooltip 仅拼接固定 `labels` 与数值;探针错误列用 Vue 文本插值,非 `v-html` |
|
||||
| H2 | 新 API / 鉴权面 | SAFE — 无后端改动;仍走既有 `/watch/metrics`、`/watch/probe-records` |
|
||||
| H3 | 密钥 / 凭据 | SAFE — 无配置或环境变量变更 |
|
||||
| H4 | 时区语义 | SAFE — 仅展示层;`formatDateTimeBeijing` / `formatChartAxisBeijing` 逻辑未改 |
|
||||
| H5 | 分页与筛选 | SAFE — `statusFilter` 变更时 `page = 1`;`pageCount` 下限为 1 |
|
||||
| H6 | 主题色一致性 | SAFE — `WatchTrendChart` 对齐 `FleetTrendEChart` 的 `useTheme` 模式 |
|
||||
| H7 | Sparkline 渐变 | P3 — `areaStyle` 使用 `${color}33` 后缀透明度,依赖主题为 `#hex`;若主题为 `rgb()` 渐变可能失效(线条仍可见) |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | SAFE | `WatchTrendChart.vue` L94–112:seriesName 来自本地 `labels`;时间经 `formatDateTimeBeijing` |
|
||||
| H2 | SAFE | diff 仅 `frontend/src/**` |
|
||||
| H3 | SAFE | 无 `server/`、`.env` 变更 |
|
||||
| H4 | SAFE | 未改 `datetime.ts` |
|
||||
| H5 | SAFE | `WatchProbeRecordsTable.vue` L88、`pageCount` computed |
|
||||
| H6 | SAFE | 与 `FleetTrendEChart.vue` 同模式 `themeColor()` |
|
||||
| H7 | P3 接受 | 与 Vuetify 默认 hex 主题兼容;后续可改为 `color-mix` 或固定 alpha 对象 |
|
||||
|
||||
## 验证
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `cd frontend && npx vite build` | ✅ 6.5s |
|
||||
| `bash scripts/local_verify.sh` | 未重跑(纯前端 UI,无 API 变更) |
|
||||
| 门控 Gate 7 文件名 | 上表已列全 8 个源码文件 basename |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog `2026-06-13-watch-ui-polish.md`(≥10 行)
|
||||
- [x] 本审计含 Step 3 / Closure / DoD
|
||||
- [x] `vite build` 通过
|
||||
- [x] 提交 `4bbc47f` + 生产部署(2026-06-13)
|
||||
- [x] 生产 `/health` ok、`/app/` 200
|
||||
- [ ] 浏览器终验:`#/watch-metrics` 暗色/亮色、探针分页、四槽空态
|
||||
|
||||
## 部署说明
|
||||
|
||||
- **仅前端**:`vite build` → `deploy/deploy-frontend.sh` 或全量 `deploy-production.sh`
|
||||
- **无需** 重启后端或数据库迁移
|
||||
|
||||
## 待办(非阻塞)
|
||||
|
||||
| 优先级 | 项 |
|
||||
|--------|-----|
|
||||
| P3 | Sparkline `areaStyle` 在 `rgb()` 主题下的渐变兼容 |
|
||||
| P3 | 探针表 `total=0` 时隐藏分页器(UX) |
|
||||
@@ -0,0 +1,35 @@
|
||||
# 审计 — 2026-06-14 门控 Gate 8 AI Review
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-gate-ai-review.md`
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 文件 | 规则 | 结论 |
|
||||
|------|------|------|
|
||||
| `scripts/gate_ai_review.py` | 无静默吞错 | PASS — 失败 exit 1 + 明确 stderr |
|
||||
| `scripts/gate_ai_review.py` | 无明文密钥 | PASS — 仅用 env CURSOR_API_KEY(标准) |
|
||||
| `scripts/gate_ai_review.py` | subprocess 安全 | PASS — 固定 argv,无 shell 拼接用户输入 |
|
||||
| `deploy/pre_deploy_check.sh` | 门控计数一致 | PASS — GATES_TOTAL=8 |
|
||||
| `docs/reviews/` | 产物可追溯 | PASS — JSON 含 head_sha、verdict、findings |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| gate_ai_review.py | SAFE | 只读 review + 写 docs/reviews |
|
||||
| pre_deploy_check.sh Gate 8 | SAFE | 调用 python 脚本 |
|
||||
| 应急跳过 | SAFE | NEXUS_SKIP_AI_REVIEW=1 显式 opt-out |
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `scripts/gate_ai_review.py`
|
||||
- `scripts/gate_ai_review.sh`
|
||||
- `deploy/pre_deploy_check.sh`
|
||||
- `docs/reviews/.gitkeep`
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Step 3 规则扫描
|
||||
- [x] Closure 表
|
||||
- [x] 文件清单与 git diff 对齐
|
||||
- [x] changelog ≥10 行
|
||||
@@ -0,0 +1,37 @@
|
||||
# 审计 — Layer3 health_monitor P2 + 监测 UI P3
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-layer3-health-monitor-p2-p3-ui.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `deploy/health_monitor.sh` | 状态目录、docker_cmd、heredoc Telegram | ☑ |
|
||||
| `tests/test_ops_patrol_sync.py` | bash -n + 路径断言 | ☑ |
|
||||
| `frontend/src/utils/echartsColor.ts` | 新建 | ☑ |
|
||||
| `WatchSparkline.vue` | rgba 渐变 | ☑ |
|
||||
| `WatchProbeRecordsTable.vue` | 空表隐藏分页 | ☑ |
|
||||
| `WatchSlotCard.vue` | ⋮ 菜单、暂停态精简(同批部署) | ☑ |
|
||||
| `WatchMetricRing.vue` | `dimmed` 占位(同批) | ☑ |
|
||||
| `server/background/watch_probe_runner.py` | 删除未使用 import | ☑ |
|
||||
|
||||
## Step 3
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | sudo docker | SAFE — 与 `nexus-1panel.sh` 同模式,失败则跳过重启 |
|
||||
| H2 | 状态目录权限 | SAFE — 在 DEPLOY_DIR 下,cron 用户可写 |
|
||||
| H3 | Telegram HTML | SAFE — heredoc 真实换行,`--data-urlencode` 不变 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 |
|
||||
|---|------|
|
||||
| H1–H3 | SAFE |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog
|
||||
- [x] `bash -n` + pytest ops_patrol
|
||||
- [x] vite build
|
||||
- [ ] 生产 L3 日志 5min 无新错误
|
||||
@@ -0,0 +1,36 @@
|
||||
# 审计 — 监测服务器详情(宝塔对齐)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-watch-baota-server-detail.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `remote_probe.py` | 负载进程数、分核 CPU、内存 MB、双 Top5 | ☑ |
|
||||
| `watch_metrics.py` | `sanitize_process_bundle`、live 字段 | ☑ |
|
||||
| `watch_service.py` | 进程 API 返回 bundle | ☑ |
|
||||
| `WatchProcessDrawer.vue` | 宝塔式详情抽屉 | ☑ |
|
||||
| `watchFormat.ts` / `useWatchPins.ts` | 类型与格式化 | ☑ |
|
||||
| `WatchSlotCard.vue` | 「详情」入口 | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 新字段单测 | ☑ |
|
||||
|
||||
## Step 3
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | SSH 脚本注入 | SAFE — 固定 psutil 脚本 |
|
||||
| H2 | API 契约 | SAFE — `processes` 兼容 list→bundle |
|
||||
| H3 | 性能 | NOTE — 分核 CPU 每 5s 一次 SSH,与现有探针同频 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 |
|
||||
|---|------|
|
||||
| H1–H3 | SAFE / 接受 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog
|
||||
- [x] pytest watch_metrics
|
||||
- [x] vite build
|
||||
- [ ] 生产终验详情抽屉
|
||||
@@ -0,0 +1,36 @@
|
||||
# 审计 — 监测开启后立即探针
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-watch-eager-probe-on-resume.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `watch_probe_runner.py` | 抽取 `_probe_server_with_pins`;`trigger_immediate_watch_probe` | ☑ |
|
||||
| `watch_service.py` | 加槽/替换/恢复监测后 eager probe | ☑ |
|
||||
| `useWatchPins.ts` | 恢复监测 3s/8s 兜底 refresh | ☑ |
|
||||
| `WatchSlotCard.vue` | 暂停底栏「已暂停」文案 | ☑ |
|
||||
| `test_watch_pins.py` | 恢复监测立即 metrics 用例 + noop mock | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无静默吞错 | SAFE — `trigger_immediate_watch_probe` 异常 `logger.exception` |
|
||||
| H2 | 探针重复/竞态 | SAFE — 与 5s 循环共用 `_probe_server_with_pins`,仅多一次即时采样 |
|
||||
| H3 | 暂停清空 Redis | SAFE — 仍仅在 `monitoring_enabled=false` 时 `clear_watch_redis_snapshot` |
|
||||
| H4 | WS 推送 | SAFE — 即时探针同样 `publish_watch_metrics` |
|
||||
| H5 | 前端轮询 | SAFE — 仅 `isWatchMetricsPending` 时 refresh,不刷屏 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H5 | SAFE | 恢复监测空窗期根因已消除;测试 10/10;local_verify 26/26 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog ≥10 行
|
||||
- [x] `pytest tests/test_watch_pins.py`
|
||||
- [x] `bash scripts/local_verify.sh`
|
||||
- [ ] 生产:开启实时监测后数秒内见指标或探针错误态
|
||||
@@ -0,0 +1,29 @@
|
||||
# 审计 — 监测槽暂停态居中开关
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-watch-slot-paused-center-switch.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `WatchSlotCard.vue` | 暂停态居中开关、开启后才显示指标 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无新 API | SAFE — 仍 `emit('monitoring', …)` |
|
||||
| H2 | 误触历史 | SAFE — 暂停态卡片主体不跳转,仅「历史」按钮 |
|
||||
| H3 | 双开关冲突 | SAFE — 顶栏开关仅 `monitoringOn` 时显示;暂停仅中部开关开启 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H3 | SAFE | 纯前端 `WatchSlotCard.vue` 模板分支 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog
|
||||
- [x] `cd frontend && npx vite build`
|
||||
- [ ] 生产浏览器终验:暂停槽中部开关 → 开启后见 CPU/内存
|
||||
@@ -0,0 +1,30 @@
|
||||
# 审计 — 监测槽暂停态 UI 与顶栏间距
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-14-watch-slot-paused-ui-polish.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `WatchSlotCard.vue` | 顶栏 ⋮ 菜单(暂停/恢复/移除);底栏暂停;暂停态 UI | ☑ |
|
||||
| `WatchMetricRing.vue` | `dimmed` 占位态 | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 无新 API | SAFE — 仍 `emit('monitoring'/'remove')` |
|
||||
| H2 | 误触移除/暂停 | SAFE — 顶栏无开关;暂停/移除在菜单内;底栏「暂停」为文字链 |
|
||||
| H3 | 无障碍 | SAFE — `aria-label` on 开关与移除 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H3 | SAFE | 纯前端 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog ≥10 行
|
||||
- [x] `vite build`
|
||||
- [ ] 生产终验:暂停态无大虚线框;开启后开关与 ✕ 不贴在一起
|
||||
@@ -0,0 +1,70 @@
|
||||
# 审计 — 监测探针 SSH 优先 + 移除 IO + 宝塔圆环 UI
|
||||
|
||||
**Changelog**:
|
||||
- `docs/changelog/2026-06-14-watch-probe-ssh-first-no-io-skip.md`
|
||||
- `docs/changelog/2026-06-14-watch-remove-io-metrics.md`
|
||||
- `docs/changelog/2026-06-14-watch-metric-ring-baota-style.md`
|
||||
|
||||
**范围**:实时监测探针与槽卡片 UI,对齐宝塔 CPU/内存/磁盘/负载展示。
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 文件 | 变更 | 状态 |
|
||||
|------|------|------|
|
||||
| `server/background/watch_probe_runner.py` | 5s 一律 SSH;去掉 IO 速率差分 | ☑ |
|
||||
| `server/utils/watch_metrics.py` | merge/live 优先 SSH;去掉 bps | ☑ |
|
||||
| `server/infrastructure/ssh/remote_probe.py` | 精简探针命令;load 补位 | ☑ |
|
||||
| `tests/test_watch_metrics.py` | 适配无 IO / SSH 优先 | ☑ |
|
||||
| `frontend/src/components/watch/WatchMetricRing.vue` | 新建圆环组件 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSlotCard.vue` | 三圆环 + 负载圆环 | ☑ |
|
||||
| `frontend/src/components/watch/WatchProbeRecordsTable.vue` | 去掉 IO 列 | ☑ |
|
||||
| `frontend/src/components/watch/WatchTrendChart.vue` | 去掉 IO 曲线 | ☑ |
|
||||
| `frontend/src/components/watch/WatchSparkline.vue` | 去掉 IO 指标 | ☑ |
|
||||
| `frontend/src/composables/useWatchPins.ts` | 类型去掉 bps | ☑ |
|
||||
| `frontend/src/pages/WatchMetricsPage.vue` | 去掉 IO 展示 | ☑ |
|
||||
| `frontend/src/utils/watchFormat.ts` | `metricRingColor` / `loadUsagePct` | ☑ |
|
||||
| `frontend/src/components/watch/WatchMetricBar.vue` | 删除(横条弃用) | ☑ |
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | SSH 凭据与命令注入 | SAFE — 仍走既有 `remote_probe` 封装;无用户输入拼入 shell |
|
||||
| H2 | 探针频率与负载 | NOTE — 有 SSH 时每 5s 一次 SSH(与宝塔同源,用户明确要求) |
|
||||
| H3 | 数据合并优先级 | SAFE — SSH 覆盖 Agent stale 心跳;SSH 全失败才回退 Agent |
|
||||
| H4 | 前端 XSS | SAFE — 圆环仅数值与固定标签;tooltip 文本插值 |
|
||||
| H5 | IO 字段移除 | SAFE — DB 列保留 NULL;API 不再推送 bps |
|
||||
| H6 | 密钥 / 凭据 | SAFE — 无 `.env` 或密钥变更 |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1 | SAFE | `remote_probe.py` 固定 psutil 脚本 |
|
||||
| H2 | 接受 | 用户要求与宝塔一致;仅监测开启槽 |
|
||||
| H3 | SAFE | `merge_redis_and_ssh` CPU/mem/disk/load 优先 SSH |
|
||||
| H4 | SAFE | `WatchMetricRing.vue` 无 `v-html` |
|
||||
| H5 | SAFE | `to_live_dict` 无 bps 键 |
|
||||
| H6 | SAFE | diff 无 secrets |
|
||||
|
||||
## 验证
|
||||
|
||||
| 项 | 结果 |
|
||||
|----|------|
|
||||
| `pytest tests/test_watch_metrics.py tests/test_watch_pins.py -q` | 22 passed(本机无 psutil 的 SSH 本地探针 1 skip/fail 为环境) |
|
||||
| `cd frontend && npx vite build` | ✅ |
|
||||
| `bash deploy/pre_deploy_check.sh` | 部署前执行 |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] changelog 三份(≥10 行)
|
||||
- [x] 本审计含 Step 3 / Closure / DoD / 文件清单
|
||||
- [x] `vite build` 通过
|
||||
- [ ] 提交并 push
|
||||
- [ ] 生产 `/health` ok、`/app/` 200
|
||||
- [ ] 浏览器终验:`#/watch-metrics` 圆环与宝塔数值接近
|
||||
|
||||
## 部署说明
|
||||
|
||||
- **后端 + 前端**:需 API 重启(Docker upgrade)+ `vite build` 同步 `web/app`
|
||||
- **无** 数据库迁移
|
||||
@@ -0,0 +1,33 @@
|
||||
# 审计 — 终端 UI 部署(2026-06-17)
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-17-terminal-ui-deploy.md`
|
||||
|
||||
## 审计范围
|
||||
|
||||
| 模块 | 文件 |
|
||||
|------|------|
|
||||
| 终端页 | `frontend/src/pages/TerminalPage.vue` |
|
||||
| 右键菜单 | `frontend/src/components/terminal/TerminalContextMenu.vue` |
|
||||
| xterm 会话 | `frontend/src/composables/terminal/useTerminalSessions.ts` |
|
||||
| 全局布局 | `frontend/src/App.vue` |
|
||||
|
||||
## Step 3
|
||||
|
||||
| H | 规则 | 结论 |
|
||||
|---|------|------|
|
||||
| H1 | 纯前端,无新 API | PASS |
|
||||
| H2 | 剪贴板仍走浏览器 API | PASS |
|
||||
| H3 | 无密钥/凭据变更 | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| H | 判定 |
|
||||
|---|------|
|
||||
| H1–H3 | PASS |
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] `local_verify.sh` 26/26
|
||||
- [x] `npx vite build`
|
||||
- [x] changelog + audit
|
||||
- [ ] 生产 `/app/` 200 + 浏览器终验
|
||||
@@ -0,0 +1,34 @@
|
||||
# 审计 — 2026-06-19 登录白名单 LOGIN_ALLOWED_IPS SSOT
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-19-login-allowlist-auth-ssot.md`
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 文件 | 规则 | 结论 |
|
||||
|------|------|------|
|
||||
| `server/utils/login_allowlist.py` | 多 worker 数据一致 | PASS — 优先 `LOGIN_ALLOWED_IPS` |
|
||||
| `server/utils/login_allowlist.py` | 无静默吞错 | PASS — 空列表回退 sub+manual |
|
||||
| `frontend/src/pages/SettingsPage.vue` | 刷新失败 UX | PASS — `refresh_ok=false` 时 `loadAllowlist` |
|
||||
| `tests/test_login_access.py` | SSOT 回归 | PASS — 新增用例 |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| login_allowlist.py | SAFE | 校验与 DB 广播合并列表一致 |
|
||||
| SettingsPage.vue | SAFE | 失败不覆盖 chip |
|
||||
| test_login_access.py | SAFE | 覆盖 SSOT 路径 |
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `server/utils/login_allowlist.py`
|
||||
- `frontend/src/pages/SettingsPage.vue`
|
||||
- `tests/test_login_access.py`
|
||||
- `docs/changelog/2026-06-19-login-allowlist-auth-ssot.md`
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Step 3 规则扫描
|
||||
- [x] Closure 表
|
||||
- [x] 文件清单与 git diff 对齐
|
||||
- [x] changelog ≥10 行
|
||||
@@ -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,78 @@
|
||||
# 审计 — 2026-06-21 宝塔 SSH 自动获取 API 部署
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-20-btpanel-ssh-api-bootstrap.md`
|
||||
**Commit**: `82426b19` feat(btpanel): 独立宝塔模块与 SSH 自动获取 API
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 文件 | 规则 | 结论 |
|
||||
|------|------|------|
|
||||
| `ssh_bootstrap.py` | 无静默吞错、远程 JSON 校验 | PASS |
|
||||
| `bootstrap_state.py` | 仅已入库服务器进 loop | PASS |
|
||||
| `btpanel_service.py` | CUD 审计 `bt_panel_ssh_bootstrap` | PASS |
|
||||
| `BtPanelSettingsPage.vue` | 批量操作二次确认 | PASS |
|
||||
| `bootstrap_lock.py` | 同机并发 SSH 互斥 | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| 后台 loop 误扫全库 | FIXED | `is_eligible` 需显式 `bt_panel` 块 |
|
||||
| `result.success` SSH | FIXED | 改用 `status == success` |
|
||||
| Permission denied 误判 | FIXED | 区分 SSH 认证与 api.json 写权限 |
|
||||
| 空凭据标 ready | FIXED | `bootstrap_empty_credentials` |
|
||||
| get_config 每次 SSH | FIXED | pending/installing 或 refresh 才探测 |
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `frontend/src/App.vue`
|
||||
- `frontend/src/api/btpanel.ts`
|
||||
- `frontend/src/components/btpanel/BtPanelPageShell.vue`
|
||||
- `frontend/src/components/btpanel/BtPanelServerPicker.vue`
|
||||
- `frontend/src/composables/btpanel/btPanelContext.ts`
|
||||
- `frontend/src/composables/btpanel/useBtPanelPageLoad.ts`
|
||||
- `frontend/src/composables/btpanel/useBtPanelServer.ts`
|
||||
- `frontend/src/constants/cachedPages.ts`
|
||||
- `frontend/src/pages/btpanel/BtPanelCrontabPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelDatabasesPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelDomainsPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelLoginPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelMonitorPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelServicesPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelSettingsPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelSiteCreatePage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelSitesPage.vue`
|
||||
- `frontend/src/pages/btpanel/BtPanelSslPage.vue`
|
||||
- `frontend/src/router/index.ts`
|
||||
- `server/api/btpanel.py`
|
||||
- `server/api/btpanel_schemas.py`
|
||||
- `server/api/servers.py`
|
||||
- `server/application/services/btpanel_bootstrap_schedule.py`
|
||||
- `server/application/services/btpanel_service.py`
|
||||
- `server/application/services/server_batch_service.py`
|
||||
- `server/background/bt_panel_bootstrap_loop.py`
|
||||
- `server/config.py`
|
||||
- `server/infrastructure/btpanel/__init__.py`
|
||||
- `server/infrastructure/btpanel/bootstrap_lock.py`
|
||||
- `server/infrastructure/btpanel/bootstrap_state.py`
|
||||
- `server/infrastructure/btpanel/client.py`
|
||||
- `server/infrastructure/btpanel/credentials.py`
|
||||
- `server/infrastructure/btpanel/source_ip.py`
|
||||
- `server/infrastructure/btpanel/ssh_bootstrap.py`
|
||||
- `server/infrastructure/btpanel/ssh_login.py`
|
||||
- `server/infrastructure/ssh/remote_shell.py`
|
||||
- `server/main.py`
|
||||
- `tests/test_btpanel_bootstrap_loop.py`
|
||||
- `tests/test_btpanel_client.py`
|
||||
- `tests/test_btpanel_get_config.py`
|
||||
- `tests/test_btpanel_ssh_bootstrap.py`
|
||||
- `tests/test_btpanel_ssh_login.py`
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Step 3 规则扫描
|
||||
- [x] Closure 表
|
||||
- [x] 文件清单与 `git diff HEAD~1..HEAD` 对齐
|
||||
- [x] `local_verify` 通过
|
||||
- [x] `tests/test_btpanel_*.py` 25 passed
|
||||
- [x] 需重启 API 注册 `bt_panel_bootstrap_loop`
|
||||
@@ -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_time,token 拼 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,34 @@
|
||||
# 审计 — 2026-06-21 登录白名单 SSOT 生产部署
|
||||
|
||||
**Changelog**: `docs/changelog/2026-06-21-login-allowlist-deploy.md`
|
||||
|
||||
## Step 3 规则扫描
|
||||
|
||||
| 文件 | 规则 | 结论 |
|
||||
|------|------|------|
|
||||
| `server/utils/login_allowlist.py` | SSOT / 多 worker | PASS |
|
||||
| `frontend/src/pages/SettingsPage.vue` | 刷新失败 UX | PASS |
|
||||
| `tests/test_login_access.py` | 回归覆盖 | PASS |
|
||||
|
||||
## Closure
|
||||
|
||||
| 项 | 状态 | 说明 |
|
||||
|----|------|------|
|
||||
| login_allowlist.py | SAFE | 优先 LOGIN_ALLOWED_IPS |
|
||||
| SettingsPage.vue | SAFE | refresh_ok=false 重载 |
|
||||
| test_login_access.py | SAFE | SSOT 用例 |
|
||||
|
||||
## 文件清单
|
||||
|
||||
- `server/utils/login_allowlist.py`
|
||||
- `frontend/src/pages/SettingsPage.vue`
|
||||
- `tests/test_login_access.py`
|
||||
- `server/infrastructure/btpanel/client.py`(bandit nosec,宝塔 API md5 协议)
|
||||
- `scripts/gate_ai_review.sh`(CRLF 修复)
|
||||
|
||||
## DoD
|
||||
|
||||
- [x] Step 3 规则扫描
|
||||
- [x] Closure 表
|
||||
- [x] 文件清单与 git diff 对齐
|
||||
- [x] changelog ≥10 行
|
||||
@@ -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 TTL;JWT 前缀白名单 | 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_time,token 拼 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 passed(watch + 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,32 @@
|
||||
# 2026-06-10 — RDP Guacamole Client.State 常量修复
|
||||
|
||||
## 摘要
|
||||
|
||||
修复浏览器 RDP 已收到 `sync` 但 UI 永远停在「连接中」:`guacamole-common-js` 1.5 运行时使用 `Client.State.CONNECTED`,而非错误的 `STATE_CONNECTED`(undefined)。
|
||||
|
||||
## 动机
|
||||
|
||||
Playwright E2E 与 WS 抓包显示 sync 已到达,但 Pinia 状态未变为 `connected`;根因是 `onstatechange` 与 `undefined` 比较永远不成立。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/composables/rdp/useRdpSession.ts` — 使用 `Client.State` / `Tunnel.State`;connect 序号防竞态
|
||||
- `frontend/src/pages/RdpPage.vue` — `mounted` 守卫避免重复 connect
|
||||
- `frontend/src/types/guacamole.d.ts` — 类型与运行时对齐
|
||||
- `frontend/e2e/pages/rdp-connect.spec.mjs` — JWT 注入登录、nav 侧栏、canvas `.first()`
|
||||
- `frontend/e2e/helpers.mjs` — `loginWithAccessToken`、`/rdp` 导航
|
||||
- `scripts/rdp_ws_probe.py`、`scripts/guacd_rdp_probe.py` — 生产 WS/guacd 探针
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
- 仅前端热更新(`web/app` sync 到容器)
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
cd frontend && npx vite build
|
||||
# 生产 Playwright(需 NEXUS_E2E_ACCESS_TOKEN 由容器 mint JWT)
|
||||
NEXUS_E2E_ACCESS_TOKEN=... npx playwright test e2e/pages/rdp-connect.spec.mjs
|
||||
```
|
||||
|
||||
生产 E2E:约 12s 内出现「已连接」+ 1024×768 画布。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 2026-06-10 — RDP 黑屏:缩放 + 键鼠
|
||||
|
||||
## 摘要
|
||||
|
||||
修复「已连接但黑屏」:Guacamole 画布默认 1024×768 未缩放,大区域为深色背景;补 `display.scale()` 适配容器、键鼠转发与 `onsync` 后重算缩放。
|
||||
|
||||
## 动机
|
||||
|
||||
E2E 显示已连接且 canvas 存在,但用户看到全黑;WS 已有 img/sync 帧,属前端渲染/布局问题。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/composables/rdp/useRdpSession.ts` — scale、Mouse/Keyboard、onsync
|
||||
- `frontend/src/pages/RdpPage.vue` — canvas 容器 flex 居中
|
||||
- `frontend/src/types/guacamole.d.ts`
|
||||
- `frontend/e2e/pages/rdp-connect.spec.mjs` — 采样非黑像素
|
||||
|
||||
## 验证
|
||||
|
||||
`NEXUS_E2E_ACCESS_TOKEN=... npx playwright test e2e/pages/rdp-connect.spec.mjs`
|
||||
|
||||
生产入口 bundle:`index-DTfFTLS8.js`;强刷后 3389 页应可见桌面且可键鼠操作。
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## 摘要
|
||||
|
||||
保留 **原 GlobalBrowserPanel 完整 UI**(标签、地址栏、canvas),固定在 App Bar 正下方全宽展示;移除地球图标、拖动、最小化、缩放角。账号仍在 App Bar 最右侧,**不**把地址栏拆进 toolbar。
|
||||
原悬浮窗保留:可拖动、最小化条、缩放;**无地球启动钮**。登录后默认收起;有标签时显示**顶栏右下**最小化条。首次无保存坐标时面板默认在**账号右侧**(顶栏下右对齐)。
|
||||
|
||||
## 动机
|
||||
|
||||
用户要的是原悬浮窗形态固定置顶,而非顶栏内嵌地址栏。
|
||||
用户只要改默认停靠位置,不要砍掉原悬浮窗能力。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 2026-06-12 移除远程内置浏览器方案
|
||||
|
||||
## 摘要
|
||||
|
||||
整体下线 Playwright Worker + 全局浮动 canvas 浏览器:删前端组件/路由、后端 `/api/browser/*` 与 `/ws/browser`、 `browser-worker/` 目录及单测。
|
||||
|
||||
## 动机
|
||||
|
||||
黑屏与状态机复杂、验证码与 SSRF 未闭环;运维场景下「站点」新标签打开即可。经验沉淀见 `docs/design/specs/2026-06-12-remote-browser-retired-lessons.md`。
|
||||
|
||||
## 涉及文件(删/改)
|
||||
|
||||
- 删:`frontend/src/components/GlobalBrowserPanel.vue`、`useGlobalBrowser.ts`、`useRemoteBrowserSession.ts`、`BrowserPage.vue`
|
||||
- 删:`server/api/browser.py`、`browser_session.py`、`server/infrastructure/browser/*`、`server/utils/browser_*`
|
||||
- 删:`browser-worker/`、`tests/test_browser_*`
|
||||
- 改:`server/main.py`(已不注册路由)、`server/config.py`(移除 `BROWSER_*`)
|
||||
- 保留:`frontend/src/utils/browserUrl.ts`;`ServersPage`「站点」→ `window.open`
|
||||
|
||||
## 迁移/重启
|
||||
|
||||
- 生产可保留 `NEXUS_BROWSER_*` 于 `.env`(`Settings.extra=ignore` 忽略);建议删除 Worker 容器与 8443 规则。
|
||||
- 需 **重启 Nexus API** 与 **前端 build 同步**。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
venv/bin/pytest tests/test_settings_sound_defaults.py -q
|
||||
cd frontend && npm run type-check && npx vite build
|
||||
curl -sf https://api.synaglobal.vip/health
|
||||
# 404 预期:GET /api/browser/status(需 JWT)
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# 2026-06-13 — 文件管理「新建文件」修复
|
||||
|
||||
## 摘要
|
||||
|
||||
修复文件页「新建文件」点击无反馈或创建后编辑器不弹出的问题。
|
||||
|
||||
## 动机
|
||||
|
||||
- 未选服务器时按钮被禁用,用户以为功能坏了
|
||||
- 创建成功后未等待 `FileEditorWorkbench` 就绪即 `openFile`,编辑器静默不打开
|
||||
- SFTP 写入前未确保父目录存在(部分路径下 write-file 失败)
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/composables/files/useFilesActions.ts` — `openNewFileDialog`、`waitForEditorWorkbench`
|
||||
- `frontend/src/composables/files/useFilesEditor.ts` — 导出 `waitForEditorWorkbench`
|
||||
- `frontend/src/composables/files/useFilesPage.ts` — 透传 helper
|
||||
- `frontend/src/components/files/FilesToolbar.vue` — 未选服务器时提示而非禁用
|
||||
- `server/infrastructure/ssh/asyncssh_pool.py` — SFTP 写前 `mkdir -p` 父目录
|
||||
|
||||
## 迁移/重启
|
||||
|
||||
- 需重启 nexus 容器 + 同步前端
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
cd frontend && npm run type-check
|
||||
# 生产:文件管理 → 选服务器 → 新建文件 → 应创建并打开编辑器
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# 修复 SSH 监测探针脚本语法错误
|
||||
|
||||
**日期**:2026-06-13
|
||||
|
||||
## 变更摘要
|
||||
|
||||
修复 `SSH_WATCH_METRICS_CMD` 中 `.strip('"')` 破坏 bash 双引号包裹导致探针全部 `ssh_probe_failed`、实时监测不可用的问题。
|
||||
|
||||
## 动机
|
||||
|
||||
硬件详情上线后监测插槽无数据;根因为嵌入脚本的 `"` 提前结束 `python3 -c "..."` 字符串。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/infrastructure/ssh/remote_probe.py` — 改用 `strip(chr(34))`
|
||||
- `tests/test_watch_metrics.py` — 本地执行探针脚本回归
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_watch_metrics.py::test_ssh_watch_metrics_cmd_runs_locally -q
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# 监测历史时间显示改为北京时间
|
||||
|
||||
**日期**:2026-06-13
|
||||
|
||||
## 变更摘要
|
||||
|
||||
监测历史页(探针记录表、资源趋势图、插槽折线 tooltip)将 API 返回的 UTC 时间按 `Asia/Shanghai` 格式化展示,与审计/告警等页一致。
|
||||
|
||||
## 动机
|
||||
|
||||
此前直接显示 UTC 的 `recorded_at` 原文,比北京时间少 8 小时,易误解。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/utils/datetime.ts` — `formatChartAxisBeijing`
|
||||
- `frontend/src/components/watch/WatchProbeRecordsTable.vue`
|
||||
- `frontend/src/components/watch/WatchTrendChart.vue`
|
||||
- `frontend/src/components/watch/WatchSparkline.vue`
|
||||
- `frontend/src/pages/WatchMetricsPage.vue`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
仅前端;`vite build` 后生效,无需重启后端。
|
||||
|
||||
## 验证方式
|
||||
|
||||
`cd frontend && npx vite build`;打开 `#/watch-metrics`,探针记录「时间」列应与本地北京时间一致。
|
||||
@@ -0,0 +1,34 @@
|
||||
# 监测插槽显示 CPU 核数 / 内存 / 硬盘容量
|
||||
|
||||
**日期**:2026-06-13
|
||||
|
||||
## 变更摘要
|
||||
|
||||
实时监测插槽展示完整硬件与用量细节:CPU 型号与物理/逻辑核、内存/磁盘已用与剩余、交换分区、系统版本与运行时长;负载显示 1/5/15 分钟。
|
||||
|
||||
## 动机
|
||||
|
||||
用户需要在插槽卡片上直观看到子机配置与实时占用,而不只有总量与使用率百分比。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/utils/watch_metrics.py` — 扩展探针字段、`fill_hardware_gaps`
|
||||
- `server/infrastructure/ssh/remote_probe.py` — SSH 探针采集详情
|
||||
- `server/background/watch_probe_runner.py` — 探针后补齐用量
|
||||
- `web/agent/agent.py` — `_hardware_specs_dict()` 心跳上报
|
||||
- `frontend/src/components/watch/WatchSlotCard.vue`、`WatchMetricBar.vue`
|
||||
- `frontend/src/utils/watchFormat.ts`、`useWatchPins.ts`
|
||||
- `tests/test_watch_metrics.py`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
无 DB 变更。SSH 监测即时生效;Agent 需升级后 Redis 通道才有完整字段(旧数据可用百分比推算已用/剩余)。
|
||||
|
||||
## 验证方式
|
||||
|
||||
```bash
|
||||
.venv/bin/pytest tests/test_watch_metrics.py -q
|
||||
cd frontend && npx vite build
|
||||
```
|
||||
|
||||
浏览器:监测槽应显示系统横幅、CPU 型号、已用/总量、可用/剩余、Swap(若有)、负载 1/5/15。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 监测槽暂停态居中开关
|
||||
|
||||
**日期**:2026-06-13
|
||||
|
||||
## 变更摘要
|
||||
|
||||
监测槽关闭实时监测时,卡片中部显示大号「开启实时监测」开关;仅开启后才展示 CPU/内存/硬盘等指标。顶栏开关仅在监测开启时显示(用于关闭)。
|
||||
|
||||
## 动机
|
||||
|
||||
暂停态原先仅一行小字提示,开关在顶栏易被忽略;用户期望未开启时中间明确引导打开。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/components/watch/WatchSlotCard.vue`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
仅前端;`vite build` 后生效。
|
||||
|
||||
## 验证方式
|
||||
|
||||
服务器页添加监测槽 → 关闭顶栏开关(或添加后默认关)→ 中部应见大号开关;打开后显示指标。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 监测 UI 视觉迭代
|
||||
|
||||
**日期**:2026-06-13
|
||||
|
||||
## 变更摘要
|
||||
|
||||
监测历史页、趋势图、探针表与实时槽卡片对齐仪表盘舰队趋势的视觉规范:主题色 ECharts、骨架屏/空态、指标色阶与 I/O 网格布局。
|
||||
|
||||
## 动机
|
||||
|
||||
北京时间上线后,监测页仍偏「工具表」风格,与仪表盘 `FleetTrendEChart` 体验不一致;继续迭代提升可读性与暗色模式适配。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `frontend/src/pages/WatchMetricsPage.vue`
|
||||
- `frontend/src/components/watch/WatchTrendChart.vue`
|
||||
- `frontend/src/components/watch/WatchProbeRecordsTable.vue`
|
||||
- `frontend/src/components/watch/WatchSlotCard.vue`
|
||||
- `frontend/src/components/watch/WatchMetricBar.vue`
|
||||
- `frontend/src/components/watch/WatchSparkline.vue`
|
||||
- `frontend/src/components/watch/WatchSlotRow.vue`
|
||||
- `frontend/src/utils/watchFormat.ts`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
仅前端;`vite build` 后生效。
|
||||
|
||||
## 验证方式
|
||||
|
||||
`cd frontend && npx vite build`;打开 `#/watch-metrics` 与服务器页监测槽,检查趋势 tooltip、探针表分页、空态与暗色主题。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user