Commit Graph

329 Commits

Author SHA1 Message Date
Your Name fd9b7d85b5 fix: 批量安装按钮检测 + 审计日志中文化 + terminal断开反馈
1. updateBatchBar() hasAgent 判断从 agent_api_key_set 改为 agent_version
2. 所有后端 AuditLog detail 字段统一中文
3. audit.html 操作/目标类型中文映射
4. terminal.html 断开覆盖层 + 重新连接按钮
5. Agent os_release 上报 + 前端系统版本拆分

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 02:17:35 +08:00
Your Name 8b82d6bab0 Add changelog for fmtTime/venv/batchinstall fixes
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:59:48 +08:00
Your Name 869f64ac18 Fix install.sh venv robustness + batch install API error reporting
install.sh: detect incomplete venv (missing activate), auto-install
python3-venv and retry instead of silently failing.

servers.py batch install: preserve server_name on exception, detect
"Status: FAILED" in stdout, return error details when stderr is empty,
increase SSH timeout to 180s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:53:00 +08:00
Your Name 69056491a7 Fix fmtTime Invalid Date for timezone-aware timestamps (+00:00)
fmtTime() was appending 'Z' to all timestamps, causing Invalid Date
when the timestamp already had a timezone suffix like +00:00.
Now tries new Date(t) first, falls back to new Date(t+'Z').

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:34:51 +08:00
Your Name 02f1fa4ef3 Add .gitattributes + changelog for Agent install fix and deploy
- .gitattributes: enforce LF line endings for .sh/.py to prevent CRLF issues on Linux
- Changelog: document Agent install 404 fix, curl|bash pipe fix, CRLF fix, and successful deployment to both sub-servers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 00:27:05 +08:00
Your Name eadf7254a1 Merge remote-tracking branch 'origin/claude/condescending-ishizaka-3cff3a' 2026-05-26 22:59:14 +08:00
Your Name 1c70e597a3 Fix Agent install 404 + add command input bar + fix install pipe error hiding
1. Mount /agent/ static files for install.sh/agent.py (was 404)
2. Replace curl|bash pipe with temp-file download to catch curl errors
3. Add command input bar to terminal.html bottom
4. Fix terminal CSS layout for flex with command bar

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 22:57:32 +08:00
Your Name 867e2aa552 Merge branch 'claude/condescending-ishizaka-3cff3a'
# Conflicts:
#	server/infrastructure/database/setting_repo.py
2026-05-26 21:58:38 +08:00
Your Name a2d392dae7 fix: WebSSH terminal echo delay — replace readline() with read()
asyncssh's async-for on shell.stdout internally calls readline()
which buffers until a newline is found. For interactive terminal
use, single-character echo never contains a newline, so all
typed characters were buffered until Enter was pressed.

Fix: use shell.stdout.read(4096) which returns data as soon as
any is available, and set bufsize=4096 on create_process() to
reduce the internal read buffer from 128KB to 4KB for low-latency
interactive echo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:54:40 +08:00
Your Name 8b10f9d64a fix: WebSSH terminal disconnect after connect + stale pool retry
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- Fix shell creation failure on stale pooled SSH connections:
  force-close and retry with fresh connection
- Fix empty error message in logs: use {type(e).__name__}: {e!r}
- Fix double WebSocket.close() causing RuntimeError in finally block:
  track ws_closed flag to avoid closing twice
- Fix Alpine undefined error in terminal.html: $d() now returns
  default object when Alpine hasn't initialized yet
- All websocket.close() calls now wrapped in try/except

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 21:33:31 +08:00
Your Name b1aa235726 fix: Gate3 false positive + test_api.py production compatibility
- Gate3: Replace loose grep (matched "0/25 failed" summary) with precise
  [FAIL] marker counting + "N test(s) failed" pattern
- test_api.py: Add .env credential loading for gate check automation
- test_api.py: Fix REST status codes (POST→201, DELETE→204)
- test_api.py: Add 204 empty body handling, dynamic ID, pre-cleanup
- Add gate_log.jsonl tracking to all 7 gates
- Add knowledge graph restructure changelog

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:33:31 +08:00
Your Name 5afb18b745 feat: gate v2 — 3→7 gates with anti-bypass, lint, security, review cross-validation
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 10:02:14 +08:00
Your Name a1b0b2c514 feat: Terminal sidebar+panel, SSH key fix, auth cleanup, pre-deploy gates
- terminal.html: left sidebar (default open), right server panel with server list
- servers.py: fix SSH key double-encryption in update_server, auth method switch cleanup
- servers.html: auth switch clears opposite credentials (key↔password)
- deploy/pre_deploy_check.sh: 3-gate pre-deploy check (changelog/audit/test)
- mcp/Nexus_server.py: deploy() runs gate check before git pull+restart
- docs/audit/: audit record template + today's audit results
- CLAUDE.md: add gate enforcement rules and progress bar discipline

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 09:16:05 +08:00
Your Name f591033309 docs: add changelog for asset management removal
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:58:14 +08:00
Your Name 89fa517693 refactor: remove asset management page, simplify server form
- Delete assets.html (platform/node management had low utility)
- Remove "资产管理" sidebar entry from layout.js
- Server form: remove Platform/Node dropdowns, keep only Category
  combobox (with datalist for existing categories)
- Remove loadOrgSelects() and platform_id/node_id from save logic
- Backend API and DB tables preserved for data compatibility

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:46:39 +08:00
Your Name 973e23e038 fix: CSV template download 401 + category combobox
1. CSV template download link now uses apiFetch (with JWT) instead
   of direct <a> href which returned 401 Unauthorized
2. Server category field changed from plain input to combobox with
   datalist — users can select existing categories or type new ones

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:30:46 +08:00
Your Name ad7f4d8d62 docs: update changelog with route fix and browser verification results
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:27:25 +08:00
Your Name fb4e51ec5a fix: move static routes before /{id} to fix route conflict
/batch/install-agent, /batch/upgrade-agent, /import, and
/import/template were registered after /{id}, causing FastAPI
to match 'batch' and 'import' as the {id} parameter.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:25:25 +08:00
Your Name 8cd091add8 feat: CSV batch import, SFTP upload, batch Agent operations
Add three missing frontend management features:
1. Server CSV import (POST /api/servers/import) with injection prevention
2. SFTP file upload (POST /api/sync/upload) with path traversal protection
3. Batch Agent install/upgrade with Semaphore concurrency control

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-26 00:17:43 +08:00
Your Name 6be989c299 feat: add SSH key preset management to credentials page
Add third tab "SSH密钥预设" to credentials.html with full CRUD:
- List presets with public key preview and creation date
- Add/edit preset (name, private key, public key)
- Reveal private key via re-authentication modal
- Delete preset with confirmation
- Route "+ 新建" button to correct form based on active tab

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 23:54:11 +08:00
Your Name ccb78ed980 feat: SSH key preset system — dropdown + backend auto-resolve
- Add SshKeyPreset model + repo (encrypted_private_key, public_key)
- Add ssh_key_preset_id to Server model + schemas
- Add CRUD + reveal API routes (/api/ssh-key-presets/)
- servers.py: auto-resolve ssh_key_preset_id → decrypt → set ssh_key_private
- servers.html: key preset dropdown in key auth section, hides path/textarea on select
- Mirror password preset pattern: zero frontend credential exposure

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 22:49:57 +08:00
Your Name 95aa8a610f security: move refresh token to HttpOnly cookie
- Refresh token stored in HttpOnly + Secure + SameSite=Lax cookie
  instead of localStorage (XSS can no longer steal it)
- Login sets cookie, refresh reads from cookie, logout clears cookie
- Access token remains in localStorage (short-lived, 30min)
- Clear pendingPassword from memory after TOTP success/back
- Remove "admin" from username placeholder
- Cookie path scoped to /api/auth (minimal exposure)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 20:22:34 +08:00
Your Name 9de6ac9904 fix: auth default to password + remove SSH tab from assets
servers.html: Default auth method to "密码" (password) instead of "密钥"
(key) when adding a new server. Hide key path row by default.

assets.html: Remove SSH Sessions tab — it duplicated the server list
functionality. Assets page now focuses on classification metadata:
platform templates and node groups only.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 19:33:39 +08:00
Your Name cf0725b037 fix: credentials.html JS parse error + assets.html SSH session UX
credentials.html: Remove broken nested ternary `Alpine?Alpine.store?...`
that caused "Unexpected token ':'" which killed the entire script block,
making all functions (showAddCred, loadCreds, etc.) undefined.

assets.html: Replace SSH session history list with server list +
double-click to open terminal, matching user's requested UX.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 19:17:09 +08:00
Your Name 3943ff4f3a fix: remove updated_at secondary check that invalidated fresh JWTs
The updated_at secondary check in _verify_token() was a "defense in
depth" mechanism, but it caused a race condition: login updates admin
(last_login, onupdate→updated_at) AFTER creating the JWT, so the JWT's
"updated" claim is immediately stale. Any DB admin update (including
innocuous ones like last_login) would invalidate all existing tokens.

The token_version primary check already covers all security scenarios
(password change, TOTP disable, token reuse detection) and is the
correct mechanism for invalidating sessions. Removed the updated_at
check from both _verify_token() and get_optional_admin().

Also reordered login/refresh flows to create JWT AFTER admin update,
so the token captures the latest updated_at (belt and suspenders).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 16:23:20 +08:00
Your Name deb3a94ee6 fix: token_version NULL handling — normalize None→0 in JWT comparison
Direct DB inserts (e.g. install wizard, manual MySQL) may leave
token_version as NULL. JWT payload had tv:null which failed the
strict != comparison against DB value 0. Normalize both sides
with `or 0` for defensive coding.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 15:37:10 +08:00
Your Name 99201f48fd fix: MissingGreenlet error — convert all middleware to pure ASGI
Root cause: BaseHTTPMiddleware.call_next() runs the endpoint in a
separate async task, which breaks SQLAlchemy's greenlet context.
After session.commit(), the connection is released to the pool.
When session.refresh() tries to acquire a new connection for a
SELECT, it fails with MissingGreenlet because the greenlet spawn
context is not available in the call_next() task.

Fix (two-part):
1. Convert all 4 middleware classes from BaseHTTPMiddleware to pure
   ASGI middleware — eliminates call_next() entirely so the entire
   request chain runs in the same async context.
2. Set expire_on_commit=False on the session factory — after commit,
   objects retain their in-memory values instead of being expired.
   This removes the need for session.refresh() entirely.

All session.refresh() calls removed from 11 repository files.
With expire_on_commit=False, post-commit attribute access no longer
triggers lazy loads that would also fail with MissingGreenlet.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 15:24:15 +08:00
Your Name c72631a293 merge: 安装脚本+PHP移除+宝塔适配 合并到main
合并 claude/condescending-ishizaka-3cff3a 分支:
- deploy/install.sh: 一键安装脚本(宝塔/标准模式自适应)
- deploy/upgrade.sh + uninstall.sh
- docs/install-guide.md: 完整中文安装教程
- PHP完全移除: install.php删除, config.php→config.json
- crypto.py: 移除PHP AES-CBC兼容, 纯Fernet
- install.py: 宝塔Supervisor路径自适应, Fernet密钥生成
- install.html: Step 5宝塔/标准模式条件渲染
- 44个冲突文件使用worktree分支版本(全部最新代码)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 08:25:29 +08:00
Your Name a1276f38ad feat: 一键安装脚本 + PHP完全移除 + config.php→config.json
新增文件:
- deploy/install.sh: 7步一键安装(自动检测宝塔/标准模式)
- deploy/upgrade.sh: git pull + pip install + restart + 健康检查
- deploy/uninstall.sh: 停服务+删配置, 可选--purge删部署目录
- docs/install-guide.md: 完整中文安装教程(宝塔+手动)

核心改动:
- install.py: 宝塔Supervisor路径自适应, init-db返回bt_panel标记,
  config.php→config.json, 移除PHP锁文件, 生成Fernet加密密钥
- install.html: Step 5根据btPanel显示不同Supervisor/Nginx/SSL指引
- crypto.py: 移除PHP AES-CBC兼容层, 纯Fernet加密
- 删除 web/install.php (1192行PHP, 功能已完全迁移到Python)

PHP清理:
- Nginx配置: config\.php→config\.json deny规则
- config.py/main.py/migrations.py: install.php注释→install wizard
- CLAUDE.md/AGENTS.md: config.php→config.json, 移除PHP引用
- firefox_server.py: login.php→login.html默认URL

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-25 08:23:37 +08:00
Your Name 400dcae781 fix: 代码验证发现的两处问题
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
- websocket.py: asyncio.get_event_loop() → get_running_loop()
  (Python 3.10+废弃,shutdown时可能RuntimeError)
- asyncssh_pool.py: 全忙等待逻辑在锁外操作有竞态条件
  简化为直接raise ConnectionError,不在锁外重试

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-24 16:31:13 +08:00
Your Name cdd0be328a fix: 六轮深度扫描 — 47项Bug修复、安全加固、死代码清理
Critical runtime bugs:
- terminal.html WebSSH完全不可用(URL前缀/JSON解析/Content-Type三处错误)
- servers.py路由遮蔽:/logs被/{id}拦截,3个前端页面同步日志查询失败
- scripts.html startExecPoll()→startExecPolling(),长任务快速执行崩溃
- agent.py {value!r!s:.50}格式串非法,agent发非数值时ValueError
- alerts.html d.daily.reduce()无null检查,API返回空数据时TypeError

Resource leak / stability:
- websocket.py僵尸连接未关闭TCP,文件描述符泄漏
- websocket.py _last_alert_time字典无限增长(加1小时过期清理)
- asyncssh_pool.py全忙时超过MAX_CONNECTIONS无限增长
- self_monitor.py Telegram告警无冷却,宕机时每30秒刷屏
- schedule_runner.py一次性调度执行超60秒会重复触发
- 限速脚本EXPIRE每次重置窗口可绕过(改用Lua原子脚本)

Security:
- JWT access token加token_version声明,改密码后旧token立失效(零宽限)
- INSTALL_MODE导入时常量→动态函数,安装后JWT认证不再残留禁用
- install.py /lock端点加管理员存在性验证,防止阻断安装
- ServerUpdate schema移除connectivity只读字段,防止伪造连接状态

Frontend fixes:
- doExec()缺r.ok检查、commands.html null检查
- _server_to_dict()补last_checked_at+ssh_key_public
- _field_match()逗号cron表达式修复
- alerts类型显示、SSH会话名称、搜索高亮定位
- 一次性/循环定时任务(run_mode+fire_at+自动禁用)

Dead code removed (400+ lines):
- SyncService batch_push/_push_single等5个方法(零调用者)
- 5个未使用schema(SyncCommands/SyncConfig/SyncSftp/FileDeploy/PaginatedResponse)
- 6个零调用service方法、3个无前端API端点
- 4个未使用import

Schema migrations:
- push_schedules: run_mode + fire_at列,cron_expr改NULL
- servers: 7个新列 + ssh_key_private/public VARCHAR(500)→TEXT

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-24 16:26:40 +08:00
Your Name fbf5eadcf2 fix(security): R2+R3 全面代码审计修复 — 3 CRITICAL / 9 HIGH / 15 MEDIUM / 10 LOW
CRITICAL:
- C-1: sync_service.py rsync命令shlex.quote防Shell注入回归
- C-2: agent.py exec端点危险命令正则拦截+compare_digest+僵尸进程修复
- C-3: WebSSH JWT token绑定server_id防IDOR

HIGH:
- H-1: 所有PUT/POST端点setattr改为字段白名单防任意字段注入
- H-2: settings/assets/scripts端点添加JWT认证
- H-3: refresh_token invalidate-then-generate防并发重放
- H-4: servers.py Redis pipeline解决N+1查询
- H-7: settings API Key/Secret掩码+禁止修改
- H-8: TOTP暴力破解速率限制(5次/5分钟)
- H-9: get_optional_admin改用request-scoped session

MEDIUM:
- M-2: X-Real-IP优先+X-Forwarded-For取末值防IP伪造
- M-4: asyncssh异常信息脱敏(只含server_id)
- M-5: 分页limit上限(500/200/500/500)
- M-9: API层Redis依赖抽象至dependencies.py
- M-10: WebSocket ConnectionManager多worker Redis聚合
- M-13: login_attempts复合索引
- M-14: ServerRepository.get_by_ids批量查询
- M-15: API错误消息统一中文

R3追加:
- sysctl命令注入防护+regex验证
- $DB_PASS明文存储→masked_command双命令方案
- MYSQL_PWD环境变量替代-p flag
- health端点拆分(公开/需认证)
- repo层**kwargs→字段白名单
- 全局str(e)信息泄露修复(API/Telegram/WebSocket)
- SSRF私有IP拦截
- Nginx HTTPS配置+DB备份脚本
- 前端CDN SRI哈希+XSS修复
- __import__替换为正常import

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:58:33 +08:00
Your Name 9bba58a529 feat: Telegram test+ChatID, Agent IP allowlist/upgrade, alert history page
Telegram:
- POST /api/settings/telegram/test: send test message (checks token+chat_id)
- GET /api/settings/telegram/chats: call getUpdates, return up to 5 chats
- settings.html: test button + detect chat IDs with click-to-fill

Agent IP allowlist (web/agent/agent.py):
- allowed_ips config: list of trusted IPs/CIDRs from config.json
- _ip_allowed(): checks exact IP, CIDR (ipaddress module), hostname
- verify_api_key(): now checks IP allowlist before API key
- /health: also checks IP allowlist
- install.sh: auto-extracts central server IP from --url, adds to allowed_ips

Agent auto-upgrade (server/api/servers.py):
- POST /api/servers/{id}/upgrade-agent: SSH → curl new agent.py → systemctl restart
- servers.html: 升级 Agent button in Agent tab (only when online)

Alert history (new page):
- domain/models: AlertLog table (server_id, type, value, is_recovery, created_at)
- migrations.py: CREATE TABLE IF NOT EXISTS alert_logs
- websocket.py: _save_alert_log() called from broadcast_alert/recovery
- settings.py: GET /api/alert-history/ (paginated, filters), GET /stats
- main.py: register alert_history_router
- alerts.html: new page with stats cards, top-servers bar chart, alert list
- layout.js: 🔔 告警中心 added to sidebar nav

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:27:12 +08:00
Your Name 488838c989 feat: separate subscription/manual IPs + master enable/disable toggle
Data model (separate storage):
- login_allowlist_enabled: 'true'/'false' master switch
- login_subscription_url: URL to fetch every 2h
- login_subscription_ips: last fetch result (replaced entirely each refresh)
- login_manual_ips: manually added (never overwritten by refresh)
- login_allowed_ips: combined (used by auth for legacy compat)

ip_allowlist_refresh.py:
- Saves to login_subscription_ips (replace) + rebuilds login_allowed_ips

auth_service.py:
- Login check: only runs when LOGIN_ALLOWLIST_ENABLED is 'true'
- Combines subscription_ips + manual_ips at auth time (no stale combined key needed)

settings.py:
- POST /ip-allowlist/toggle: quick enable/disable endpoint
- GET /ip-allowlist: returns subscription_ips, manual_ips separately
- POST /ip-allowlist: saves manual_ips + optional subscription_url + optional enabled

settings.html:
- Toggle switch: enable/disable with warning if list is empty
- Green notice when enabled (warns about self-lockout risk)
- Grey notice when disabled
- Subscription IPs section: read-only, auto-refresh badge
- Manual IPs section: deletable per-IP, clear all button

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:19:47 +08:00
Your Name da43960f38 feat: subscription URL stored in settings, auto-refresh allowlist every 2h
config.py:
- LOGIN_SUBSCRIPTION_URL: stored in DB, configurable from UI
- LOGIN_MANUAL_IPS: manually added IPs (preserved across auto-refresh)

background/ip_allowlist_refresh.py (new):
- ip_allowlist_refresh_loop(): primary-worker task, every 2h
- _do_refresh(): fetch subscription, parse IPs, merge with manual IPs,
  write combined to LOGIN_ALLOWED_IPS + DB setting; runs once at startup (5s delay)
- get_last_refresh_time(): returns last successful refresh timestamp

main.py:
- Register ip_allowlist_refresh_loop as background task

settings.py:
- GET /ip-allowlist: now returns subscription_url, manual_ips, last_refresh
- POST /ip-allowlist: saves manual_ips + subscription_url, triggers refresh
- POST /ip-allowlist/manual: append to manual list only, trigger rebuild
- DELETE /ip-allowlist/ip: remove single manual IP, trigger rebuild

settings.html:
- Subscription URL field with 保存 + 🔄 instant-refresh button
- Last refresh timestamp display
- Status badge shows '订阅自动刷新' when URL configured
- IP badges: blue=manual (deletable), grey=from subscription (auto)
- 预览 button to inspect subscription IPs without saving

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:15:29 +08:00
Your Name 5fcc1e22a6 feat: login IP allowlist with proxy subscription parser
server/infrastructure/subscription_parser.py (new):
- parse_subscription_content(): fetch and decode subscription, extract host/IP
- Supports SS, VMess, VLESS, Trojan, Hysteria2 formats
- Base64 decode with padding fix; IPv4/CIDR/hostname detection

server/config.py:
- LOGIN_ALLOWED_IPS setting (empty = allow all, comma-separated)

server/api/settings.py:
- POST /settings/ip-allowlist/parse-subscription: fetch URL, decode, return hosts
- POST /settings/ip-allowlist: save allowlist to settings table + reload
- GET /settings/ip-allowlist: return current list

server/application/services/auth_service.py:
- _ip_in_allowlist(): supports exact IP, CIDR (ipaddress module), hostname match
- login(): check allowlist before brute-force check; if IP blocked return
  reason='ip_blocked' with clear message; audit log 'login_ip_blocked'

web/app/settings.html:
- New '登录 IP 白名单' card with:
  - Subscription URL input + parse button (calls backend parser)
  - Parsed IPs with checkboxes (select/deselect before adding)
  - Manual IP/CIDR/hostname textarea input
  - Current allowlist with per-IP remove buttons
  - Clear all button (removes restriction)
  - Status badge: 启用 N条 / 未启用(不限制)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 18:11:23 +08:00
Your Name 37ceaef6d9 feat: auto-poll Agent online status after install with live badge update
- remoteInstallAgent(): after success, start _startAgentOnlinePoller() instead
  of one-shot setTimeout(loadServers, 2000)
- _startAgentOnlinePoller(serverId): poll GET /api/servers/{id} every 5s up to
  2 minutes (24 attempts); when is_online=true, update status badge live,
  disable remote-install button, toast 'Agent 已上线', reload server list
- _updateAgentStatusBadge(isOnline): in-place badge update without full reload
- Install log shows: '✓ 安装成功!等待首次心跳...' then ' Agent 心跳已收到'
- Hint text explains: install ~30-60s, first heartbeat another ~60s
- Manual install: 'click to wait for Agent online' button triggers same poller

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:47:22 +08:00
Your Name d414b945e1 feat: audit log time-range filter + script execution schedules
Audit log time-range filter:
- audit_log_repo.py: unified query() with action/admin_username/date_from/date_to
  filters + total count; _build_filters() helper for date parsing
- settings.py GET /audit/: add date_from, date_to, admin_username query params
- audit.html: date range inputs (YYYY-MM-DD), admin filter input, action filter
  expanded with optgroups (服务器/推送/脚本/认证/设置); clear filter button

Script execution schedules:
- domain/models: PushSchedule extended with schedule_type('push'|'script'),
  script_id FK, script_content Text, exec_timeout int, long_task bool;
  source_path made nullable
- migrations.py: 6 new ALTER TABLE statements (idempotent)
- schedule_runner.py: detect schedule_type; for 'script' type use ScriptService;
  for 'push' type keep original SyncEngineV2 sync_files behaviour
- schemas.py: ScheduleCreate/Update add all new fields
- schedules.html: full UI rewrite
  - type selector radio: 📁 文件推送 /  脚本执行
  - script fields: script library dropdown + textarea + timeout + long_task
  - schedule list shows type badge + detail preview
  - saveSched() validates required fields per type

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:29:16 +08:00
Your Name 4ca07fa252 feat: quick execute panel in scripts.html (no script save required)
Add collapsible '直接执行命令' card at top of scripts page:
- Textarea for ad-hoc command (no script_id, not saved to library)
- Multi-select server list (loaded on first expand, same batch size)
- Options: timeout, DB credential (* substitution), long_task nohup
- Ctrl+Enter shortcut to submit
- Uses same batch tracking + status panel + polling as saved scripts
- Clear button to reset command
- Batch hint shows server count and batch size
- 全选 button for quick server selection

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:23:25 +08:00
Your Name d2832567c7 feat: push logs view in commands.html + paginated sync log API
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
sync_log_repo.py:
- get_all_paginated(): server_id/status/sync_mode/trigger_type filters +
  server name JOIN + total count for pagination

server/api/servers.py GET /servers/logs:
- Add server_id/status/sync_mode/trigger_type/offset/limit query params
- Return {items, total, offset, limit} instead of plain list

web/app/commands.html (renamed title to '操作日志'):
- Third view: '推送日志' with paginated table
- Columns: 状态(dot badge) / 服务器 / 模式 / 触发方式 / 源路径 /
  目标路径 / 文件数 / 耗时 / 操作人 / 开始时间
- Filters: 服务器 + 状态(success/failed/running) + 模式 (incremental/full/...)
- Pagination controls with prev/next page (50 per page)
- Error message in row title attribute (hover for details)

web/app/push.html:
- Update loadHistory() to handle new {items, total} response format

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:20:53 +08:00
Your Name 14dc29f241 feat: Agent install tab with copy command + one-click remote install via SSH
server/api/servers.py:
- POST /{id}/install-agent: SSH-execute install.sh on managed server using
  stored credentials; requires API_BASE_URL + agent_api_key; 120s timeout
- GET /{id}/agent-install-cmd: return pre-filled curl command for copy-paste

web/app/servers.html:
- New 'Agent 安装' tab in server detail panel
  - Shows Agent online/offline status badge
  - Warns if NEXUS_API_BASE_URL or API Key is missing
  - Displays ready-to-copy curl install command with port auto-filled
  - ' 一键远程安装' button (SSH exec): disabled if already online
  - Scrollable install log output panel
  - Tab uses shared showDetailTab() + loadAgentInstall() / remoteInstallAgent()
- Explains: script auto-installs Python, downloads agent.py, writes config,
  sets up systemd, opens firewall port

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:16:14 +08:00
Your Name b9e8db7a3f feat: per-alert notification toggles with switch UI
config.py:
- Add NOTIFY_ALERT_CPU/MEM/DISK, NOTIFY_RECOVERY, NOTIFY_TIME_DRIFT,
  NOTIFY_SYSTEM_REDIS, NOTIFY_SYSTEM_MYSQL, NOTIFY_RESTART settings
  (string 'true'/'false', loaded from MySQL settings table at startup)

telegram/__init__.py:
- _notify_enabled(): check setting string ('false'/'0'/'no'/'off' = disabled)
- send_telegram_alert(): checks NOTIFY_ALERT_{CPU|MEM|DISK} per alert_type
- send_telegram_recovery(): checks NOTIFY_RECOVERY
- send_telegram_system_alert(): new notify_key param, checks matching setting
- send_telegram_restart_*(): check NOTIFY_RESTART

self_monitor.py:
- Pass notify_key='NOTIFY_SYSTEM_REDIS/MYSQL' to send_telegram_system_alert

server/api/agent.py:
- Pass notify_key='NOTIFY_TIME_DRIFT' to time-drift alert

settings.html:
- New '告警通知项目' section with 8 toggle switches (pill style)
- Grouped by category: 服务器资源 / 服务器 / 系统
- Optimistic toggle with revert on API failure
- renderNotifyToggles() reads _allSettings and toggleNotify() saves instantly

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:12:07 +08:00
Your Name 08d0baf762 feat: TOTP re-bind support + improved settings UI
settings.html TOTP section overhaul:
- Status cards: green (enabled) / amber (disabled) with context-aware buttons
- Enabled state: add '重新绑定' button to change Authenticator app/device
  without disabling first (same API, just new QR + verify flow)
- Disabled state: prominent 'Enable TOTP' CTA with warning about security
- Bind panel: shared between first-time setup and re-bind, auto-scrolls into view
- Auto-focus verify input after QR renders; Enter key submits
- QR generation: 180px, transparent background, fallback message if QRious fails
- Re-bind label distinguishes 'rebind success' from 'first enable' toast
- Disable flow: clearer confirm dialog mentioning security implication

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:07:40 +08:00
Your Name 9c063a788f feat: time drift detection between agent and central server
Agent (web/agent/agent.py):
- Heartbeat now includes system_info.agent_time (UTC ISO string)

Central (server/api/agent.py):
- On each heartbeat, compare agent_time vs central datetime.now(utc)
- Thresholds: warn >= 30s, crit >= 60s (affects TOTP/cron accuracy)
- Stores time_drift_seconds + drift_level in Redis heartbeat key
- Sends Telegram alert (5-min cooldown) when drift >= 60s critical

API (server/api/servers.py):
- List and detail endpoints expose time_drift_seconds + drift_level from Redis

Frontend (web/app/servers.html):
- driftBadge(): shows clock emoji badge next to server name
  - amber badge: >=30s (warn)
  - red badge: >=60s (crit)
  - tooltip shows exact drift and NTP check hint
  - no badge when drift < 30s or no data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:04:34 +08:00
Your Name 5a42345bc0 docs: Nexus 6.0 功能说明书 HTML + roadmap stale fix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:55:24 +08:00
Your Name 6c5e6a2daa feat: file operations (delete/rename/mkdir) + fix roadmap Step3 status
Backend:
- schemas.py: FileOperation model (server_id, operation, path, new_path)
- sync_v2.py: POST /api/sync/file-ops endpoint
  - delete: rm -rf with shlex.quote
  - rename: mv src -> dst with shlex.quote on both paths
  - mkdir: mkdir -p with shlex.quote
  - all operations audit-logged, 30s timeout

Frontend (files.html):
- Header: add 📁+ button (doMkdir via prompt)
- File rows: action buttons (rename + delete) appear on hover
  - rename: prompt for new name, inline mv
  - delete: confirm dialog with warning for dirs (rm -rf)
  - all ops refresh current directory after success
- fileOp() shared helper for POST /api/sync/file-ops

Docs:
- roadmap.md: Step 3 Web SSH updated from 70% to completed

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:46:00 +08:00
Your Name ac0eb79707 feat: rsync dry-run preview (方案D 智能提示型)
Backend:
- schemas.py: SyncPreview model (server_id, source/target_path, sync_mode, verbose)
- sync_engine_v2.py: preview_sync() + _parse_rsync_stats() helpers
  - runs rsync --dry-run --stats; verbose=True adds -v (capped 200 lines)
  - returns stats dict: files_transferred/created/deleted + byte counts
- sync_v2.py: POST /api/sync/preview endpoint with audit log

Frontend (push.html - 方案D):
- Sync mode radio change listener: onSyncModeChange()
- full/overwrite mode: orange warning card + prominent preview button
- incremental/checksum mode: subtle "预览首台变动 →" link
- Inline preview result section (no modal): stats cards + optional file list
- files_deleted shown in red card for destructive modes
- verbose toggle: "显示详细文件列表 ▼" triggers second request with verbose=true
- fmtBytes() helper for human-readable file sizes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:41:47 +08:00
Your Name e37bc6cea9 docs: drop file editor (Monaco/ACE) - officially rejected
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:31:33 +08:00
Your Name 2c6f3abbd0 docs: add standards/ folder with all reusable dev & audit standards
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:23:50 +08:00
Your Name d5c498549b docs: system development standard v1.0 - full coding rules for Python/FastAPI/Frontend
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 16:22:36 +08:00